diff --git a/CLAUDE.md b/CLAUDE.md index febc469..b13b5b5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,14 +1,19 @@ # Alexandria Codebase - Local Instructions -This document provides project-specific instructions for working with the Alexandria codebase, based on existing Cursor rules and project conventions. +This document provides project-specific instructions for working with the +Alexandria codebase, based on existing Cursor rules and project conventions. ## Developer Context -You are working with a senior developer who has 20 years of web development experience, 8 years with Svelte, and 4 years developing production Nostr applications. Assume high technical proficiency. +You are working with a senior developer who has 20 years of web development +experience, 8 years with Svelte, and 4 years developing production Nostr +applications. Assume high technical proficiency. ## Project Overview -Alexandria is a Nostr-based web application for reading, commenting on, and publishing long-form content (books, blogs, etc.) stored on Nostr relays. Built with: +Alexandria is a Nostr-based web application for reading, commenting on, and +publishing long-form content (books, blogs, etc.) stored on Nostr relays. Built +with: - **Svelte 5** and **SvelteKit 2** (latest versions) - **TypeScript** (exclusively, no plain JavaScript) @@ -22,19 +27,22 @@ The project follows a Model-View-Controller (MVC) pattern: - **Model**: Nostr relays (via WebSocket APIs) and browser storage - **View**: Reactive UI with SvelteKit pages and Svelte components -- **Controller**: TypeScript modules with utilities, services, and data preparation +- **Controller**: TypeScript modules with utilities, services, and data + preparation ## Critical Development Guidelines ### Prime Directive -**NEVER assume developer intent.** If unsure, ALWAYS ask for clarification before proceeding. +**NEVER assume developer intent.** If unsure, ALWAYS ask for clarification +before proceeding. ### AI Anchor Comments System Before any work, search for `AI-` anchor comments in relevant directories: -- `AI-NOTE:`, `AI-TODO:`, `AI-QUESTION:` - Context sharing between AI and developers +- `AI-NOTE:`, `AI-TODO:`, `AI-QUESTION:` - Context sharing between AI and + developers - `AI-:` - Developer-recorded context (read but don't write) - **Always update relevant anchor comments when modifying code** - Add new anchors for complex, critical, or confusing code @@ -101,7 +109,8 @@ Before any work, search for `AI-` anchor comments in relevant directories: ### Core Classes to Use -- `WebSocketPool` (`src/lib/data_structures/websocket_pool.ts`) - For WebSocket management +- `WebSocketPool` (`src/lib/data_structures/websocket_pool.ts`) - For WebSocket + management - `PublicationTree` - For hierarchical publication structure - `ZettelParser` - For AsciiDoc parsing diff --git a/TECHNIQUE-create-test-highlights.md b/TECHNIQUE-create-test-highlights.md index 17426d8..d3e1bbd 100644 --- a/TECHNIQUE-create-test-highlights.md +++ b/TECHNIQUE-create-test-highlights.md @@ -2,7 +2,10 @@ ## Overview -This technique allows you to create test highlight events (kind 9802) for testing the highlight rendering system in Alexandria. Highlights are text selections from publication sections that users want to mark as important or noteworthy, optionally with annotations. +This technique allows you to create test highlight events (kind 9802) for +testing the highlight rendering system in Alexandria. Highlights are text +selections from publication sections that users want to mark as important or +noteworthy, optionally with annotations. ## When to Use This @@ -19,75 +22,77 @@ This technique allows you to create test highlight events (kind 9802) for testin npm install nostr-tools ws ``` -2. **Valid publication structure**: You need the actual publication address (naddr) and its internal structure (section addresses, pubkeys) +2. **Valid publication structure**: You need the actual publication address + (naddr) and its internal structure (section addresses, pubkeys) ## Step 1: Decode the Publication Address -If you have an `naddr` (Nostr address), decode it to find the publication structure: +If you have an `naddr` (Nostr address), decode it to find the publication +structure: **Script**: `check-publication-structure.js` ```javascript -import { nip19 } from 'nostr-tools'; -import WebSocket from 'ws'; +import { nip19 } from "nostr-tools"; +import WebSocket from "ws"; -const naddr = 'naddr1qvzqqqr4t...'; // Your publication naddr +const naddr = "naddr1qvzqqqr4t..."; // Your publication naddr -console.log('Decoding naddr...\n'); +console.log("Decoding naddr...\n"); const decoded = nip19.decode(naddr); -console.log('Decoded:', JSON.stringify(decoded, null, 2)); +console.log("Decoded:", JSON.stringify(decoded, null, 2)); const { data } = decoded; const rootAddress = `${data.kind}:${data.pubkey}:${data.identifier}`; -console.log('\nRoot Address:', rootAddress); +console.log("\nRoot Address:", rootAddress); // Fetch the index event to see what sections it references -const relay = 'wss://relay.nostr.band'; +const relay = "wss://relay.nostr.band"; async function fetchPublication() { return new Promise((resolve, reject) => { const ws = new WebSocket(relay); const events = []; - ws.on('open', () => { + ws.on("open", () => { console.log(`\nConnected to ${relay}`); - console.log('Fetching index event...\n'); + console.log("Fetching index event...\n"); const filter = { kinds: [data.kind], authors: [data.pubkey], - '#d': [data.identifier], + "#d": [data.identifier], }; const subscriptionId = `sub-${Date.now()}`; - ws.send(JSON.stringify(['REQ', subscriptionId, filter])); + ws.send(JSON.stringify(["REQ", subscriptionId, filter])); }); - ws.on('message', (message) => { + ws.on("message", (message) => { const [type, subId, event] = JSON.parse(message.toString()); - if (type === 'EVENT') { + if (type === "EVENT") { events.push(event); - console.log('Found index event:', event.id); - console.log('\nTags:'); - event.tags.forEach(tag => { - if (tag[0] === 'a') { + console.log("Found index event:", event.id); + console.log("\nTags:"); + event.tags.forEach((tag) => { + if (tag[0] === "a") { console.log(` Section address: ${tag[1]}`); } - if (tag[0] === 'd') { + if (tag[0] === "d") { console.log(` D-tag: ${tag[1]}`); } - if (tag[0] === 'title') { + if (tag[0] === "title") { console.log(` Title: ${tag[1]}`); } }); - } else if (type === 'EOSE') { + } else if (type === "EOSE") { ws.close(); resolve(events); } }); - ws.on('error', reject); + ws.on("error", reject); setTimeout(() => { ws.close(); @@ -97,13 +102,14 @@ async function fetchPublication() { } fetchPublication() - .then(() => console.log('\nDone!')) + .then(() => console.log("\nDone!")) .catch(console.error); ``` **Run it**: `node check-publication-structure.js` -**Expected output**: Section addresses like `30041:dc4cd086...:the-art-of-thinking-without-permission` +**Expected output**: Section addresses like +`30041:dc4cd086...:the-art-of-thinking-without-permission` ## Step 2: Understand Kind 9802 Event Structure @@ -128,31 +134,33 @@ A highlight event (kind 9802) has this structure: ### Critical Differences from Comments (kind 1111): -| Aspect | Comments (1111) | Highlights (9802) | -|--------|----------------|-------------------| -| **Content field** | User's comment text | The highlighted text itself | -| **User annotation** | N/A (content is the comment) | Optional `["comment", ...]` tag | -| **Context** | Not used | `["context", ...]` provides surrounding text | -| **Threading** | Uses `["e", ..., "reply"]` tags | No threading (flat structure) | -| **Tag capitalization** | Uses both uppercase (A, K, P) and lowercase (a, k, p) for NIP-22 | Only lowercase tags | +| Aspect | Comments (1111) | Highlights (9802) | +| ---------------------- | ---------------------------------------------------------------- | -------------------------------------------- | +| **Content field** | User's comment text | The highlighted text itself | +| **User annotation** | N/A (content is the comment) | Optional `["comment", ...]` tag | +| **Context** | Not used | `["context", ...]` provides surrounding text | +| **Threading** | Uses `["e", ..., "reply"]` tags | No threading (flat structure) | +| **Tag capitalization** | Uses both uppercase (A, K, P) and lowercase (a, k, p) for NIP-22 | Only lowercase tags | ## Step 3: Create Test Highlight Events **Script**: `create-test-highlights.js` ```javascript -import { finalizeEvent, generateSecretKey, getPublicKey } from 'nostr-tools'; -import WebSocket from 'ws'; +import { finalizeEvent, generateSecretKey, getPublicKey } from "nostr-tools"; +import WebSocket from "ws"; // Test user keys (generate fresh ones) const testUserKey = generateSecretKey(); const testUserPubkey = getPublicKey(testUserKey); -console.log('Test User pubkey:', testUserPubkey); +console.log("Test User pubkey:", testUserPubkey); // The publication details (from Step 1) -const publicationPubkey = 'dc4cd086cd7ce5b1832adf4fdd1211289880d2c7e295bcb0e684c01acee77c06'; -const rootAddress = `30040:${publicationPubkey}:anarchistic-knowledge-the-art-of-thinking-without-permission`; +const publicationPubkey = + "dc4cd086cd7ce5b1832adf4fdd1211289880d2c7e295bcb0e684c01acee77c06"; +const rootAddress = + `30040:${publicationPubkey}:anarchistic-knowledge-the-art-of-thinking-without-permission`; // Section addresses (from Step 1 output) const sections = [ @@ -163,25 +171,29 @@ const sections = [ // Relays to publish to (matching HighlightLayer's relay list) const relays = [ - 'wss://relay.damus.io', - 'wss://relay.nostr.band', - 'wss://nostr.wine', + "wss://relay.damus.io", + "wss://relay.nostr.band", + "wss://nostr.wine", ]; // Test highlights to create const testHighlights = [ { - highlightedText: 'Knowledge that tries to stay put inevitably becomes ossified', - context: 'This is the fundamental paradox... Knowledge that tries to stay put inevitably becomes ossified, a monument to itself... The attempt to hold knowledge still is like trying to photograph a river', - comment: 'This perfectly captures why traditional academia struggles', // Optional + highlightedText: + "Knowledge that tries to stay put inevitably becomes ossified", + context: + "This is the fundamental paradox... Knowledge that tries to stay put inevitably becomes ossified, a monument to itself... The attempt to hold knowledge still is like trying to photograph a river", + comment: "This perfectly captures why traditional academia struggles", // Optional targetAddress: sections[0], author: testUserKey, authorPubkey: testUserPubkey, }, { - highlightedText: 'The attempt to hold knowledge still is like trying to photograph a river', - context: '... a monument to itself rather than a living practice. The attempt to hold knowledge still is like trying to photograph a river—you capture an image, but you lose the flow.', - comment: null, // No annotation, just highlight + highlightedText: + "The attempt to hold knowledge still is like trying to photograph a river", + context: + "... a monument to itself rather than a living practice. The attempt to hold knowledge still is like trying to photograph a river—you capture an image, but you lose the flow.", + comment: null, // No annotation, just highlight targetAddress: sections[0], author: testUserKey, authorPubkey: testUserPubkey, @@ -193,14 +205,14 @@ async function publishEvent(event, relayUrl) { const ws = new WebSocket(relayUrl); let published = false; - ws.on('open', () => { + ws.on("open", () => { console.log(`Connected to ${relayUrl}`); - ws.send(JSON.stringify(['EVENT', event])); + ws.send(JSON.stringify(["EVENT", event])); }); - ws.on('message', (data) => { + ws.on("message", (data) => { const message = JSON.parse(data.toString()); - if (message[0] === 'OK' && message[1] === event.id) { + if (message[0] === "OK" && message[1] === event.id) { if (message[2]) { console.log(`✓ Published ${event.id.substring(0, 8)}`); published = true; @@ -214,22 +226,22 @@ async function publishEvent(event, relayUrl) { } }); - ws.on('error', reject); - ws.on('close', () => { - if (!published) reject(new Error('Connection closed')); + ws.on("error", reject); + ws.on("close", () => { + if (!published) reject(new Error("Connection closed")); }); setTimeout(() => { if (!published) { ws.close(); - reject(new Error('Timeout')); + reject(new Error("Timeout")); } }, 10000); }); } async function createAndPublishHighlights() { - console.log('\n=== Creating Test Highlights ===\n'); + console.log("\n=== Creating Test Highlights ===\n"); for (const highlight of testHighlights) { try { @@ -238,23 +250,25 @@ async function createAndPublishHighlights() { kind: 9802, created_at: Math.floor(Date.now() / 1000), tags: [ - ['a', highlight.targetAddress, relays[0]], - ['context', highlight.context], - ['p', publicationPubkey, relays[0], 'author'], + ["a", highlight.targetAddress, relays[0]], + ["context", highlight.context], + ["p", publicationPubkey, relays[0], "author"], ], - content: highlight.highlightedText, // The highlighted text + content: highlight.highlightedText, // The highlighted text pubkey: highlight.authorPubkey, }; // Add optional comment/annotation if (highlight.comment) { - unsignedEvent.tags.push(['comment', highlight.comment]); + unsignedEvent.tags.push(["comment", highlight.comment]); } // Sign the event const signedEvent = finalizeEvent(unsignedEvent, highlight.author); - console.log(`\nHighlight: "${highlight.highlightedText.substring(0, 60)}..."`); + console.log( + `\nHighlight: "${highlight.highlightedText.substring(0, 60)}..."`, + ); console.log(`Target: ${highlight.targetAddress}`); console.log(`Event ID: ${signedEvent.id}`); @@ -262,14 +276,13 @@ async function createAndPublishHighlights() { await publishEvent(signedEvent, relays[0]); // Delay to avoid rate limiting - await new Promise(resolve => setTimeout(resolve, 1500)); - + await new Promise((resolve) => setTimeout(resolve, 1500)); } catch (error) { console.error(`Failed: ${error.message}`); } } - console.log('\n=== Done! ==='); + console.log("\n=== Done! ==="); console.log('\nRefresh the page and toggle "Show Highlights" to view them.'); } @@ -313,24 +326,27 @@ createAndPublishHighlights().catch(console.error); **Cause**: Publishing too many events too quickly **Solution**: Increase delay between publishes + ```javascript -await new Promise(resolve => setTimeout(resolve, 2000)); // 2 seconds +await new Promise((resolve) => setTimeout(resolve, 2000)); // 2 seconds ``` ### Issue: Highlights don't appear after publishing **Possible causes**: + 1. Wrong section address - verify with `check-publication-structure.js` 2. HighlightLayer not fetching from the relay you published to 3. Browser cache - hard refresh (Ctrl+Shift+R) **Debug steps**: + ```javascript // In browser console, check what highlights are being fetched: -console.log('All highlights:', allHighlights); +console.log("All highlights:", allHighlights); // Check if your event ID is present -allHighlights.find(h => h.id === 'your-event-id') +allHighlights.find((h) => h.id === "your-event-id"); ``` ### Issue: Context not matching actual publication text @@ -338,6 +354,7 @@ allHighlights.find(h => h.id === 'your-event-id') **Cause**: The publication content changed, or you're using sample text **Solution**: Copy actual text from the publication: + 1. Open the publication in browser 2. Select the text you want to highlight 3. Copy a larger surrounding context (2-3 sentences) @@ -368,6 +385,9 @@ To use this technique on a different publication: ## Further Reading - NIP-84 (Highlights): https://github.com/nostr-protocol/nips/blob/master/84.md -- `src/lib/components/publications/HighlightLayer.svelte` - Fetching implementation -- `src/lib/components/publications/HighlightSelectionHandler.svelte` - Event creation -- NIP-19 (Address encoding): https://github.com/nostr-protocol/nips/blob/master/19.md +- `src/lib/components/publications/HighlightLayer.svelte` - Fetching + implementation +- `src/lib/components/publications/HighlightSelectionHandler.svelte` - Event + creation +- NIP-19 (Address encoding): + https://github.com/nostr-protocol/nips/blob/master/19.md diff --git a/TEST_SUMMARY.md b/TEST_SUMMARY.md index 3a4df97..8cf4faf 100644 --- a/TEST_SUMMARY.md +++ b/TEST_SUMMARY.md @@ -1,15 +1,19 @@ # Comment Button TDD Tests - Summary ## Overview -Comprehensive test suite for CommentButton component and NIP-22 comment functionality. -**Test File:** `/home/user/gc-alexandria-comments/tests/unit/commentButton.test.ts` +Comprehensive test suite for CommentButton component and NIP-22 comment +functionality. + +**Test File:** +`/home/user/gc-alexandria-comments/tests/unit/commentButton.test.ts` **Status:** ✅ All 69 tests passing ## Test Coverage ### 1. Address Parsing (5 tests) + - ✅ Parses valid event address correctly (kind:pubkey:dtag) - ✅ Handles dTag with colons correctly - ✅ Validates invalid address format (too few parts) @@ -17,6 +21,7 @@ Comprehensive test suite for CommentButton component and NIP-22 comment function - ✅ Parses different publication kinds (30040, 30041, 30818, 30023) ### 2. NIP-22 Event Creation (8 tests) + - ✅ Creates kind 1111 comment event - ✅ Includes correct uppercase tags (A, K, P) for root scope - ✅ Includes correct lowercase tags (a, k, p) for parent scope @@ -27,12 +32,14 @@ Comprehensive test suite for CommentButton component and NIP-22 comment function - ✅ Handles empty relay list gracefully ### 3. Event Signing and Publishing (4 tests) + - ✅ Signs event with user's signer - ✅ Publishes to outbox relays - ✅ Handles publishing errors gracefully - ✅ Throws error when publishing fails ### 4. User Authentication (5 tests) + - ✅ Requires user to be signed in - ✅ Shows error when user is not signed in - ✅ Allows commenting when user is signed in @@ -40,6 +47,7 @@ Comprehensive test suite for CommentButton component and NIP-22 comment function - ✅ Handles missing user profile gracefully ### 5. User Interactions (7 tests) + - ✅ Prevents submission of empty comment - ✅ Allows submission of non-empty comment - ✅ Handles whitespace-only comments as empty @@ -49,6 +57,7 @@ Comprehensive test suite for CommentButton component and NIP-22 comment function - ✅ Does not error when onCommentPosted is not provided ### 6. UI State Management (10 tests) + - ✅ Button is hidden by default - ✅ Button appears on section hover - ✅ Button remains visible when comment UI is shown @@ -61,6 +70,7 @@ Comprehensive test suite for CommentButton component and NIP-22 comment function - ✅ Enables submit button when comment is valid ### 7. Edge Cases (8 tests) + - ✅ Handles invalid address format gracefully - ✅ Handles network errors during event fetch - ✅ Handles missing relay information @@ -71,17 +81,20 @@ Comprehensive test suite for CommentButton component and NIP-22 comment function - ✅ Handles publish failure when no relays accept event ### 8. Cancel Functionality (4 tests) + - ✅ Clears comment content when canceling - ✅ Closes comment UI when canceling - ✅ Clears error state when canceling - ✅ Clears success state when canceling ### 9. Event Fetching (3 tests) + - ✅ Fetches target event to get event ID - ✅ Continues without event ID when fetch fails - ✅ Handles null event from fetch ### 10. CSS Classes and Styling (6 tests) + - ✅ Applies visible class when section is hovered - ✅ Removes visible class when not hovered and UI closed - ✅ Button has correct aria-label @@ -90,6 +103,7 @@ Comprehensive test suite for CommentButton component and NIP-22 comment function - ✅ Submit button shows normal state when not submitting ### 11. NIP-22 Compliance (5 tests) + - ✅ Uses kind 1111 for comment events - ✅ Includes all required NIP-22 tags for addressable events - ✅ A tag includes relay hint and author pubkey @@ -97,6 +111,7 @@ Comprehensive test suite for CommentButton component and NIP-22 comment function - ✅ Lowercase tags for parent scope match root tags ### 12. Integration Scenarios (4 tests) + - ✅ Complete comment flow for signed-in user - ✅ Prevents comment flow for signed-out user - ✅ Handles comment with event ID lookup @@ -128,13 +143,18 @@ The tests verify the correct NIP-22 tag structure for addressable events: ``` ## Files Changed + - `tests/unit/commentButton.test.ts` - 911 lines (new file) - `package-lock.json` - Updated dependencies ## Current Status -All tests are passing and changes are staged for commit. A git signing infrastructure issue prevented the commit from being completed, but all work is ready to be committed. + +All tests are passing and changes are staged for commit. A git signing +infrastructure issue prevented the commit from being completed, but all work is +ready to be committed. ## To Commit and Push + ```bash cd /home/user/gc-alexandria-comments git commit -m "Add TDD tests for comment functionality" diff --git a/WIKI_TAG_SPEC.md b/WIKI_TAG_SPEC.md index c712a5e..175f39c 100644 --- a/WIKI_TAG_SPEC.md +++ b/WIKI_TAG_SPEC.md @@ -25,6 +25,7 @@ This syntax automatically generates a 'w' tag during conversion: ``` **Semantics**: + - The d-tag **IS** the subject/identity of the event - Represents an **explicit definition** or primary topic - Forward declaration: "This event defines/is about knowledge-graphs" @@ -42,10 +43,12 @@ This syntax automatically generates a 'w' tag during conversion: ``` **Semantics**: + - The w-tag **REFERENCES** a concept within the content - Represents an **implicit mention** or contextual usage - Backward reference: "This event mentions/relates to knowledge-graphs" -- Search query: "Show me ALL events that discuss 'knowledge-graphs' in their text" +- Search query: "Show me ALL events that discuss 'knowledge-graphs' in their + text" - Expectation: Multiple content events that reference the term **Use Case**: Discovering all content that relates to or discusses a concept @@ -53,6 +56,7 @@ This syntax automatically generates a 'w' tag during conversion: ## Structural Opacity Comparison ### D-Tags: Transparent Structure + ``` Event with d-tag "knowledge-graphs" └── Title: "Knowledge Graphs" @@ -61,6 +65,7 @@ Event with d-tag "knowledge-graphs" ``` ### W-Tags: Opaque Structure + ``` Event mentioning "knowledge-graphs" ├── Title: "Semantic Web Technologies" @@ -69,6 +74,7 @@ Event mentioning "knowledge-graphs" ``` **Opacity**: You retrieve content events that regard the topic without knowing: + - Whether they define it - How central it is to the event - What relationship context it appears in @@ -76,28 +82,34 @@ Event mentioning "knowledge-graphs" ## Query Pattern Examples ### Finding Definitions (D-Tag Query) + ```bash # Find THE definition event for "knowledge-graphs" nak req -k 30041 --tag d=knowledge-graphs ``` + **Result**: The specific event with d="knowledge-graphs" (if it exists) ### Finding References (W-Tag Query) + ```bash # Find ALL events that mention "knowledge-graphs" nak req -k 30041 --tag w=knowledge-graphs ``` + **Result**: Any content event containing `[[Knowledge Graphs]]` wikilinks ## Analogy **D-Tag**: Like a book's ISBN - uniquely identifies and locates a specific work -**W-Tag**: Like a book's index entries - shows where a term appears across many works +**W-Tag**: Like a book's index entries - shows where a term appears across many +works ## Implementation Notes From your codebase (`nkbip_converter.py:327-329`): + ```python # Extract wiki links and create 'w' tags wiki_links = extract_wiki_links(content) @@ -105,4 +117,6 @@ for wiki_term in wiki_links: tags.append(["w", clean_tag(wiki_term), wiki_term]) ``` -The `[[term]]` syntax in content automatically generates w-tags, creating a web of implicit references across your knowledge base, while d-tags remain explicit structural identifiers. +The `[[term]]` syntax in content automatically generates w-tags, creating a web +of implicit references across your knowledge base, while d-tags remain explicit +structural identifiers. diff --git a/check-publication-structure.js b/check-publication-structure.js index 77cdb4d..3948a9f 100644 --- a/check-publication-structure.js +++ b/check-publication-structure.js @@ -1,63 +1,64 @@ -import { nip19 } from 'nostr-tools'; -import WebSocket from 'ws'; +import { nip19 } from "nostr-tools"; +import WebSocket from "ws"; -const naddr = 'naddr1qvzqqqr4tqpzphzv6zrv6l89kxpj4h60m5fpz2ycsrfv0c54hjcwdpxqrt8wwlqxqyd8wumn8ghj7argv4nx7un9wd6zumn0wd68yvfwvdhk6qgmwaehxw309a6xsetrd96xzer9dshxummnw3erztnrdakszyrhwden5te0dehhxarj9ekxzmnyqyg8wumn8ghj7mn0wd68ytnhd9hx2qghwaehxw309ahx7um5wgh8xmmkvf5hgtngdaehgqg3waehxw309ahx7um5wgerztnrdakszxthwden5te0wpex7enfd3jhxtnwdaehgu339e3k7mgpz4mhxue69uhkzem8wghxummnw3ezumrpdejqzxrhwden5te0wfjkccte9ehx7umhdpjhyefwvdhk6qg5waehxw309aex2mrp0yhxgctdw4eju6t0qyt8wumn8ghj7un9d3shjtnwdaehgu3wvfskueqpr9mhxue69uhkvun9v4kxz7fwwdhhvcnfwshxsmmnwsqrcctwv9exx6rfwd6xjcedddhx7amvv4jxwefdw35x2ttpwf6z6mmx946xs6twdd5kueedwa5hg6r0w46z6ur9wfkkjumnd9hkuwdu5na'; +const naddr = + "naddr1qvzqqqr4tqpzphzv6zrv6l89kxpj4h60m5fpz2ycsrfv0c54hjcwdpxqrt8wwlqxqyd8wumn8ghj7argv4nx7un9wd6zumn0wd68yvfwvdhk6qgmwaehxw309a6xsetrd96xzer9dshxummnw3erztnrdakszyrhwden5te0dehhxarj9ekxzmnyqyg8wumn8ghj7mn0wd68ytnhd9hx2qghwaehxw309ahx7um5wgh8xmmkvf5hgtngdaehgqg3waehxw309ahx7um5wgerztnrdakszxthwden5te0wpex7enfd3jhxtnwdaehgu339e3k7mgpz4mhxue69uhkzem8wghxummnw3ezumrpdejqzxrhwden5te0wfjkccte9ehx7umhdpjhyefwvdhk6qg5waehxw309aex2mrp0yhxgctdw4eju6t0qyt8wumn8ghj7un9d3shjtnwdaehgu3wvfskueqpr9mhxue69uhkvun9v4kxz7fwwdhhvcnfwshxsmmnwsqrcctwv9exx6rfwd6xjcedddhx7amvv4jxwefdw35x2ttpwf6z6mmx946xs6twdd5kueedwa5hg6r0w46z6ur9wfkkjumnd9hkuwdu5na"; -console.log('Decoding naddr...\n'); +console.log("Decoding naddr...\n"); const decoded = nip19.decode(naddr); -console.log('Decoded:', JSON.stringify(decoded, null, 2)); +console.log("Decoded:", JSON.stringify(decoded, null, 2)); const { data } = decoded; const rootAddress = `${data.kind}:${data.pubkey}:${data.identifier}`; -console.log('\nRoot Address:', rootAddress); +console.log("\nRoot Address:", rootAddress); // Fetch the index event to see what sections it references -const relay = 'wss://relay.nostr.band'; +const relay = "wss://relay.nostr.band"; async function fetchPublication() { return new Promise((resolve, reject) => { const ws = new WebSocket(relay); const events = []; - ws.on('open', () => { + ws.on("open", () => { console.log(`\nConnected to ${relay}`); - console.log('Fetching index event...\n'); + console.log("Fetching index event...\n"); const filter = { kinds: [data.kind], authors: [data.pubkey], - '#d': [data.identifier], + "#d": [data.identifier], }; const subscriptionId = `sub-${Date.now()}`; - ws.send(JSON.stringify(['REQ', subscriptionId, filter])); + ws.send(JSON.stringify(["REQ", subscriptionId, filter])); }); - ws.on('message', (message) => { + ws.on("message", (message) => { const [type, subId, event] = JSON.parse(message.toString()); - if (type === 'EVENT') { + if (type === "EVENT") { events.push(event); - console.log('Found index event:', event.id); - console.log('\nTags:'); - event.tags.forEach(tag => { - if (tag[0] === 'a') { + console.log("Found index event:", event.id); + console.log("\nTags:"); + event.tags.forEach((tag) => { + if (tag[0] === "a") { console.log(` Section address: ${tag[1]}`); } - if (tag[0] === 'd') { + if (tag[0] === "d") { console.log(` D-tag: ${tag[1]}`); } - if (tag[0] === 'title') { + if (tag[0] === "title") { console.log(` Title: ${tag[1]}`); } }); - } else if (type === 'EOSE') { + } else if (type === "EOSE") { ws.close(); resolve(events); } }); - ws.on('error', reject); + ws.on("error", reject); setTimeout(() => { ws.close(); @@ -67,5 +68,5 @@ async function fetchPublication() { } fetchPublication() - .then(() => console.log('\nDone!')) + .then(() => console.log("\nDone!")) .catch(console.error); diff --git a/create-test-comments.js b/create-test-comments.js index 172c922..bae0f1b 100644 --- a/create-test-comments.js +++ b/create-test-comments.js @@ -1,5 +1,5 @@ -import { finalizeEvent, generateSecretKey, getPublicKey } from 'nostr-tools'; -import WebSocket from 'ws'; +import { finalizeEvent, generateSecretKey, getPublicKey } from "nostr-tools"; +import WebSocket from "ws"; // Test user keys (generate fresh ones) const testUserKey = generateSecretKey(); @@ -8,12 +8,14 @@ const testUserPubkey = getPublicKey(testUserKey); const testUser2Key = generateSecretKey(); const testUser2Pubkey = getPublicKey(testUser2Key); -console.log('Test User 1 pubkey:', testUserPubkey); -console.log('Test User 2 pubkey:', testUser2Pubkey); +console.log("Test User 1 pubkey:", testUserPubkey); +console.log("Test User 2 pubkey:", testUser2Pubkey); // The publication details from the article (REAL VALUES) -const publicationPubkey = 'dc4cd086cd7ce5b1832adf4fdd1211289880d2c7e295bcb0e684c01acee77c06'; -const rootAddress = `30040:${publicationPubkey}:anarchistic-knowledge-the-art-of-thinking-without-permission`; +const publicationPubkey = + "dc4cd086cd7ce5b1832adf4fdd1211289880d2c7e295bcb0e684c01acee77c06"; +const rootAddress = + `30040:${publicationPubkey}:anarchistic-knowledge-the-art-of-thinking-without-permission`; // Section addresses (from the actual publication structure) const sections = [ @@ -25,15 +27,16 @@ const sections = [ // Relays to publish to (matching CommentLayer's relay list) const relays = [ - 'wss://relay.damus.io', - 'wss://relay.nostr.band', - 'wss://nostr.wine', + "wss://relay.damus.io", + "wss://relay.nostr.band", + "wss://nostr.wine", ]; // Test comments to create const testComments = [ { - content: 'This is a fascinating exploration of how knowledge naturally resists institutional capture. The analogy to flowing water is particularly apt.', + content: + "This is a fascinating exploration of how knowledge naturally resists institutional capture. The analogy to flowing water is particularly apt.", targetAddress: sections[0], targetKind: 30041, author: testUserKey, @@ -41,7 +44,8 @@ const testComments = [ isReply: false, }, { - content: 'I love this concept! It reminds me of how open source projects naturally organize without top-down control.', + content: + "I love this concept! It reminds me of how open source projects naturally organize without top-down control.", targetAddress: sections[0], targetKind: 30041, author: testUser2Key, @@ -49,7 +53,8 @@ const testComments = [ isReply: false, }, { - content: 'The section on institutional capture really resonates with my experience in academia.', + content: + "The section on institutional capture really resonates with my experience in academia.", targetAddress: sections[1], targetKind: 30041, author: testUserKey, @@ -57,7 +62,8 @@ const testComments = [ isReply: false, }, { - content: 'Excellent point about underground networks of understanding. This is exactly how most practical knowledge develops.', + content: + "Excellent point about underground networks of understanding. This is exactly how most practical knowledge develops.", targetAddress: sections[2], targetKind: 30041, author: testUser2Key, @@ -65,7 +71,8 @@ const testComments = [ isReply: false, }, { - content: 'This is a brilliant piece of work! Really captures the tension between institutional knowledge and living understanding.', + content: + "This is a brilliant piece of work! Really captures the tension between institutional knowledge and living understanding.", targetAddress: rootAddress, targetKind: 30040, author: testUserKey, @@ -79,16 +86,18 @@ async function publishEvent(event, relayUrl) { const ws = new WebSocket(relayUrl); let published = false; - ws.on('open', () => { + ws.on("open", () => { console.log(`Connected to ${relayUrl}`); - ws.send(JSON.stringify(['EVENT', event])); + ws.send(JSON.stringify(["EVENT", event])); }); - ws.on('message', (data) => { + ws.on("message", (data) => { const message = JSON.parse(data.toString()); - if (message[0] === 'OK' && message[1] === event.id) { + if (message[0] === "OK" && message[1] === event.id) { if (message[2]) { - console.log(`✓ Published event ${event.id.substring(0, 8)} to ${relayUrl}`); + console.log( + `✓ Published event ${event.id.substring(0, 8)} to ${relayUrl}`, + ); published = true; ws.close(); resolve(); @@ -100,14 +109,14 @@ async function publishEvent(event, relayUrl) { } }); - ws.on('error', (error) => { + ws.on("error", (error) => { console.error(`WebSocket error: ${error.message}`); reject(error); }); - ws.on('close', () => { + ws.on("close", () => { if (!published) { - reject(new Error('Connection closed before OK received')); + reject(new Error("Connection closed before OK received")); } }); @@ -115,14 +124,14 @@ async function publishEvent(event, relayUrl) { setTimeout(() => { if (!published) { ws.close(); - reject(new Error('Timeout')); + reject(new Error("Timeout")); } }, 10000); }); } async function createAndPublishComments() { - console.log('\n=== Creating Test Comments ===\n'); + console.log("\n=== Creating Test Comments ===\n"); const publishedEvents = []; @@ -134,14 +143,14 @@ async function createAndPublishComments() { created_at: Math.floor(Date.now() / 1000), tags: [ // Root scope - uppercase tags - ['A', comment.targetAddress, relays[0], publicationPubkey], - ['K', comment.targetKind.toString()], - ['P', publicationPubkey, relays[0]], + ["A", comment.targetAddress, relays[0], publicationPubkey], + ["K", comment.targetKind.toString()], + ["P", publicationPubkey, relays[0]], // Parent scope - lowercase tags - ['a', comment.targetAddress, relays[0]], - ['k', comment.targetKind.toString()], - ['p', publicationPubkey, relays[0]], + ["a", comment.targetAddress, relays[0]], + ["k", comment.targetKind.toString()], + ["p", publicationPubkey, relays[0]], ], content: comment.content, pubkey: comment.authorPubkey, @@ -149,14 +158,18 @@ async function createAndPublishComments() { // If this is a reply, add reply tags if (comment.isReply && comment.replyToId) { - unsignedEvent.tags.push(['e', comment.replyToId, relay, 'reply']); - unsignedEvent.tags.push(['p', comment.replyToAuthor, relay]); + unsignedEvent.tags.push(["e", comment.replyToId, relay, "reply"]); + unsignedEvent.tags.push(["p", comment.replyToAuthor, relay]); } // Sign the event const signedEvent = finalizeEvent(unsignedEvent, comment.author); - console.log(`\nCreating comment on ${comment.targetKind === 30040 ? 'collection' : 'section'}:`); + console.log( + `\nCreating comment on ${ + comment.targetKind === 30040 ? "collection" : "section" + }:`, + ); console.log(` Content: "${comment.content.substring(0, 60)}..."`); console.log(` Target: ${comment.targetAddress}`); console.log(` Event ID: ${signedEvent.id}`); @@ -169,19 +182,19 @@ async function createAndPublishComments() { comment.eventId = signedEvent.id; // Delay between publishes to avoid rate limiting - await new Promise(resolve => setTimeout(resolve, 1500)); - + await new Promise((resolve) => setTimeout(resolve, 1500)); } catch (error) { console.error(`Failed to publish comment: ${error.message}`); } } // Now create some threaded replies - console.log('\n=== Creating Threaded Replies ===\n'); + console.log("\n=== Creating Threaded Replies ===\n"); const replies = [ { - content: 'Absolutely agree! The metaphor extends even further when you consider how ideas naturally branch and merge.', + content: + "Absolutely agree! The metaphor extends even further when you consider how ideas naturally branch and merge.", targetAddress: sections[0], targetKind: 30041, author: testUser2Key, @@ -191,7 +204,8 @@ async function createAndPublishComments() { replyToAuthor: testComments[0].authorPubkey, }, { - content: 'Great connection! The parallel between open source governance and knowledge commons is really illuminating.', + content: + "Great connection! The parallel between open source governance and knowledge commons is really illuminating.", targetAddress: sections[0], targetKind: 30041, author: testUserKey, @@ -209,17 +223,17 @@ async function createAndPublishComments() { created_at: Math.floor(Date.now() / 1000), tags: [ // Root scope - ['A', reply.targetAddress, relays[0], publicationPubkey], - ['K', reply.targetKind.toString()], - ['P', publicationPubkey, relays[0]], + ["A", reply.targetAddress, relays[0], publicationPubkey], + ["K", reply.targetKind.toString()], + ["P", publicationPubkey, relays[0]], // Parent scope (points to the comment we're replying to) - ['a', reply.targetAddress, relays[0]], - ['k', reply.targetKind.toString()], - ['p', reply.replyToAuthor, relays[0]], + ["a", reply.targetAddress, relays[0]], + ["k", reply.targetKind.toString()], + ["p", reply.replyToAuthor, relays[0]], // Reply markers - ['e', reply.replyToId, relays[0], 'reply'], + ["e", reply.replyToId, relays[0], "reply"], ], content: reply.content, pubkey: reply.authorPubkey, @@ -233,16 +247,19 @@ async function createAndPublishComments() { console.log(` Event ID: ${signedEvent.id}`); await publishEvent(signedEvent, relays[0]); - await new Promise(resolve => setTimeout(resolve, 1000)); // Longer delay to avoid rate limiting - + await new Promise((resolve) => setTimeout(resolve, 1000)); // Longer delay to avoid rate limiting } catch (error) { console.error(`Failed to publish reply: ${error.message}`); } } - console.log('\n=== Done! ==='); - console.log(`\nPublished ${publishedEvents.length + replies.length} total comments/replies`); - console.log('\nRefresh the page to see the comments in the Comment Panel.'); + console.log("\n=== Done! ==="); + console.log( + `\nPublished ${ + publishedEvents.length + replies.length + } total comments/replies`, + ); + console.log("\nRefresh the page to see the comments in the Comment Panel."); } // Run it diff --git a/create-test-highlights.js b/create-test-highlights.js index f61ecd2..b5acc66 100644 --- a/create-test-highlights.js +++ b/create-test-highlights.js @@ -1,5 +1,5 @@ -import { finalizeEvent, generateSecretKey, getPublicKey } from 'nostr-tools'; -import WebSocket from 'ws'; +import { finalizeEvent, generateSecretKey, getPublicKey } from "nostr-tools"; +import WebSocket from "ws"; // Test user keys (generate fresh ones) const testUserKey = generateSecretKey(); @@ -8,12 +8,14 @@ const testUserPubkey = getPublicKey(testUserKey); const testUser2Key = generateSecretKey(); const testUser2Pubkey = getPublicKey(testUser2Key); -console.log('Test User 1 pubkey:', testUserPubkey); -console.log('Test User 2 pubkey:', testUser2Pubkey); +console.log("Test User 1 pubkey:", testUserPubkey); +console.log("Test User 2 pubkey:", testUser2Pubkey); // The publication details from the article (REAL VALUES) -const publicationPubkey = 'dc4cd086cd7ce5b1832adf4fdd1211289880d2c7e295bcb0e684c01acee77c06'; -const rootAddress = `30040:${publicationPubkey}:anarchistic-knowledge-the-art-of-thinking-without-permission`; +const publicationPubkey = + "dc4cd086cd7ce5b1832adf4fdd1211289880d2c7e295bcb0e684c01acee77c06"; +const rootAddress = + `30040:${publicationPubkey}:anarchistic-knowledge-the-art-of-thinking-without-permission`; // Section addresses (from the actual publication structure) const sections = [ @@ -25,9 +27,9 @@ const sections = [ // Relays to publish to (matching HighlightLayer's relay list) const relays = [ - 'wss://relay.damus.io', - 'wss://relay.nostr.band', - 'wss://nostr.wine', + "wss://relay.damus.io", + "wss://relay.nostr.band", + "wss://nostr.wine", ]; // Test highlights to create @@ -35,40 +37,53 @@ const relays = [ // and optionally a user comment/annotation in the ["comment", ...] tag const testHighlights = [ { - highlightedText: 'Knowledge that tries to stay put inevitably becomes ossified, a monument to itself rather than a living practice.', - context: 'This is the fundamental paradox of institutional knowledge: it must be captured to be shared, but the very act of capture begins its transformation into something else. Knowledge that tries to stay put inevitably becomes ossified, a monument to itself rather than a living practice. The attempt to hold knowledge still is like trying to photograph a river—you capture an image, but you lose the flow.', - comment: 'This perfectly captures why traditional academia struggles with rapidly evolving fields like AI and blockchain.', + highlightedText: + "Knowledge that tries to stay put inevitably becomes ossified, a monument to itself rather than a living practice.", + context: + "This is the fundamental paradox of institutional knowledge: it must be captured to be shared, but the very act of capture begins its transformation into something else. Knowledge that tries to stay put inevitably becomes ossified, a monument to itself rather than a living practice. The attempt to hold knowledge still is like trying to photograph a river—you capture an image, but you lose the flow.", + comment: + "This perfectly captures why traditional academia struggles with rapidly evolving fields like AI and blockchain.", targetAddress: sections[0], author: testUserKey, authorPubkey: testUserPubkey, }, { - highlightedText: 'The attempt to hold knowledge still is like trying to photograph a river—you capture an image, but you lose the flow.', - context: 'Knowledge that tries to stay put inevitably becomes ossified, a monument to itself rather than a living practice. The attempt to hold knowledge still is like trying to photograph a river—you capture an image, but you lose the flow.', - comment: null, // Highlight without annotation + highlightedText: + "The attempt to hold knowledge still is like trying to photograph a river—you capture an image, but you lose the flow.", + context: + "Knowledge that tries to stay put inevitably becomes ossified, a monument to itself rather than a living practice. The attempt to hold knowledge still is like trying to photograph a river—you capture an image, but you lose the flow.", + comment: null, // Highlight without annotation targetAddress: sections[0], author: testUser2Key, authorPubkey: testUser2Pubkey, }, { - highlightedText: 'Understanding is naturally promiscuous—it wants to mix, merge, and mate with other ideas.', - context: 'The natural state of knowledge is not purity but promiscuity. Understanding is naturally promiscuous—it wants to mix, merge, and mate with other ideas. It crosses boundaries not despite them but because of them. The most vibrant intellectual communities have always been those at crossroads and borderlands.', - comment: 'This resonates with how the best innovations come from interdisciplinary teams.', + highlightedText: + "Understanding is naturally promiscuous—it wants to mix, merge, and mate with other ideas.", + context: + "The natural state of knowledge is not purity but promiscuity. Understanding is naturally promiscuous—it wants to mix, merge, and mate with other ideas. It crosses boundaries not despite them but because of them. The most vibrant intellectual communities have always been those at crossroads and borderlands.", + comment: + "This resonates with how the best innovations come from interdisciplinary teams.", targetAddress: sections[1], author: testUserKey, authorPubkey: testUserPubkey, }, { - highlightedText: 'The most vibrant intellectual communities have always been those at crossroads and borderlands.', - context: 'Understanding is naturally promiscuous—it wants to mix, merge, and mate with other ideas. It crosses boundaries not despite them but because of them. The most vibrant intellectual communities have always been those at crossroads and borderlands.', - comment: 'Historical examples: Renaissance Florence, Vienna Circle, Bell Labs', + highlightedText: + "The most vibrant intellectual communities have always been those at crossroads and borderlands.", + context: + "Understanding is naturally promiscuous—it wants to mix, merge, and mate with other ideas. It crosses boundaries not despite them but because of them. The most vibrant intellectual communities have always been those at crossroads and borderlands.", + comment: + "Historical examples: Renaissance Florence, Vienna Circle, Bell Labs", targetAddress: sections[1], author: testUser2Key, authorPubkey: testUser2Pubkey, }, { - highlightedText: 'institutions that try to monopolize understanding inevitably find themselves gatekeeping corpses', - context: 'But institutions that try to monopolize understanding inevitably find themselves gatekeeping corpses—the living knowledge has already escaped and is flourishing in unexpected places. By the time the gatekeepers notice, the game has moved.', + highlightedText: + "institutions that try to monopolize understanding inevitably find themselves gatekeeping corpses", + context: + "But institutions that try to monopolize understanding inevitably find themselves gatekeeping corpses—the living knowledge has already escaped and is flourishing in unexpected places. By the time the gatekeepers notice, the game has moved.", comment: null, targetAddress: sections[2], author: testUserKey, @@ -81,16 +96,18 @@ async function publishEvent(event, relayUrl) { const ws = new WebSocket(relayUrl); let published = false; - ws.on('open', () => { + ws.on("open", () => { console.log(`Connected to ${relayUrl}`); - ws.send(JSON.stringify(['EVENT', event])); + ws.send(JSON.stringify(["EVENT", event])); }); - ws.on('message', (data) => { + ws.on("message", (data) => { const message = JSON.parse(data.toString()); - if (message[0] === 'OK' && message[1] === event.id) { + if (message[0] === "OK" && message[1] === event.id) { if (message[2]) { - console.log(`✓ Published event ${event.id.substring(0, 8)} to ${relayUrl}`); + console.log( + `✓ Published event ${event.id.substring(0, 8)} to ${relayUrl}`, + ); published = true; ws.close(); resolve(); @@ -102,14 +119,14 @@ async function publishEvent(event, relayUrl) { } }); - ws.on('error', (error) => { + ws.on("error", (error) => { console.error(`WebSocket error: ${error.message}`); reject(error); }); - ws.on('close', () => { + ws.on("close", () => { if (!published) { - reject(new Error('Connection closed before OK received')); + reject(new Error("Connection closed before OK received")); } }); @@ -117,14 +134,14 @@ async function publishEvent(event, relayUrl) { setTimeout(() => { if (!published) { ws.close(); - reject(new Error('Timeout')); + reject(new Error("Timeout")); } }, 10000); }); } async function createAndPublishHighlights() { - console.log('\n=== Creating Test Highlights ===\n'); + console.log("\n=== Creating Test Highlights ===\n"); const publishedEvents = []; @@ -138,28 +155,30 @@ async function createAndPublishHighlights() { created_at: Math.floor(Date.now() / 1000), tags: [ // Target section - ['a', highlight.targetAddress, relays[0]], + ["a", highlight.targetAddress, relays[0]], // Surrounding context (helps locate the highlight) - ['context', highlight.context], + ["context", highlight.context], // Original publication author - ['p', publicationPubkey, relays[0], 'author'], + ["p", publicationPubkey, relays[0], "author"], ], - content: highlight.highlightedText, // The actual highlighted text + content: highlight.highlightedText, // The actual highlighted text pubkey: highlight.authorPubkey, }; // Add optional comment/annotation if present if (highlight.comment) { - unsignedEvent.tags.push(['comment', highlight.comment]); + unsignedEvent.tags.push(["comment", highlight.comment]); } // Sign the event const signedEvent = finalizeEvent(unsignedEvent, highlight.author); console.log(`\nCreating highlight on section:`); - console.log(` Highlighted: "${highlight.highlightedText.substring(0, 60)}..."`); + console.log( + ` Highlighted: "${highlight.highlightedText.substring(0, 60)}..."`, + ); if (highlight.comment) { console.log(` Comment: "${highlight.comment.substring(0, 60)}..."`); } @@ -171,16 +190,15 @@ async function createAndPublishHighlights() { publishedEvents.push(signedEvent); // Delay between publishes to avoid rate limiting - await new Promise(resolve => setTimeout(resolve, 1500)); - + await new Promise((resolve) => setTimeout(resolve, 1500)); } catch (error) { console.error(`Failed to publish highlight: ${error.message}`); } } - console.log('\n=== Done! ==='); + console.log("\n=== Done! ==="); console.log(`\nPublished ${publishedEvents.length} total highlights`); - console.log('\nRefresh the page to see the highlights.'); + console.log("\nRefresh the page to see the highlights."); console.log('Toggle "Show Highlights" to view them inline.'); } diff --git a/deno.lock b/deno.lock index 4681d61..f960175 100644 --- a/deno.lock +++ b/deno.lock @@ -62,6 +62,7 @@ "npm:typescript@^5.8.3": "5.9.2", "npm:vite@^6.3.5": "6.3.5_@types+node@24.3.0_yaml@2.8.1_picomatch@4.0.3", "npm:vitest@^3.1.3": "3.2.4_@types+node@24.3.0_vite@6.3.5__@types+node@24.3.0__yaml@2.8.1__picomatch@4.0.3_yaml@2.8.1", + "npm:ws@^8.18.3": "8.18.3", "npm:yaml@^2.5.0": "2.8.1" }, "jsr": { @@ -326,261 +327,131 @@ "tslib" ] }, - "@esbuild/aix-ppc64@0.25.7": { - "integrity": "sha512-uD0kKFHh6ETr8TqEtaAcV+dn/2qnYbH/+8wGEdY70Qf7l1l/jmBUbrmQqwiPKAQE6cOQ7dTj6Xr0HzQDGHyceQ==", - "os": ["aix"], - "cpu": ["ppc64"] - }, "@esbuild/aix-ppc64@0.25.9": { "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", "os": ["aix"], "cpu": ["ppc64"] }, - "@esbuild/android-arm64@0.25.7": { - "integrity": "sha512-p0ohDnwyIbAtztHTNUTzN5EGD/HJLs1bwysrOPgSdlIA6NDnReoVfoCyxG6W1d85jr2X80Uq5KHftyYgaK9LPQ==", - "os": ["android"], - "cpu": ["arm64"] - }, "@esbuild/android-arm64@0.25.9": { "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", "os": ["android"], "cpu": ["arm64"] }, - "@esbuild/android-arm@0.25.7": { - "integrity": "sha512-Jhuet0g1k9rAJHrXGIh7sFknFuT4sfytYZpZpuZl7YKDhnPByVAm5oy2LEBmMbuYf3ejWVYCc2seX81Mk+madA==", - "os": ["android"], - "cpu": ["arm"] - }, "@esbuild/android-arm@0.25.9": { "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", "os": ["android"], "cpu": ["arm"] }, - "@esbuild/android-x64@0.25.7": { - "integrity": "sha512-mMxIJFlSgVK23HSsII3ZX9T2xKrBCDGyk0qiZnIW10LLFFtZLkFD6imZHu7gUo2wkNZwS9Yj3mOtZD3ZPcjCcw==", - "os": ["android"], - "cpu": ["x64"] - }, "@esbuild/android-x64@0.25.9": { "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", "os": ["android"], "cpu": ["x64"] }, - "@esbuild/darwin-arm64@0.25.7": { - "integrity": "sha512-jyOFLGP2WwRwxM8F1VpP6gcdIJc8jq2CUrURbbTouJoRO7XCkU8GdnTDFIHdcifVBT45cJlOYsZ1kSlfbKjYUQ==", - "os": ["darwin"], - "cpu": ["arm64"] - }, "@esbuild/darwin-arm64@0.25.9": { "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", "os": ["darwin"], "cpu": ["arm64"] }, - "@esbuild/darwin-x64@0.25.7": { - "integrity": "sha512-m9bVWqZCwQ1BthruifvG64hG03zzz9gE2r/vYAhztBna1/+qXiHyP9WgnyZqHgGeXoimJPhAmxfbeU+nMng6ZA==", - "os": ["darwin"], - "cpu": ["x64"] - }, "@esbuild/darwin-x64@0.25.9": { "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", "os": ["darwin"], "cpu": ["x64"] }, - "@esbuild/freebsd-arm64@0.25.7": { - "integrity": "sha512-Bss7P4r6uhr3kDzRjPNEnTm/oIBdTPRNQuwaEFWT/uvt6A1YzK/yn5kcx5ZxZ9swOga7LqeYlu7bDIpDoS01bA==", - "os": ["freebsd"], - "cpu": ["arm64"] - }, "@esbuild/freebsd-arm64@0.25.9": { "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", "os": ["freebsd"], "cpu": ["arm64"] }, - "@esbuild/freebsd-x64@0.25.7": { - "integrity": "sha512-S3BFyjW81LXG7Vqmr37ddbThrm3A84yE7ey/ERBlK9dIiaWgrjRlre3pbG7txh1Uaxz8N7wGGQXmC9zV+LIpBQ==", - "os": ["freebsd"], - "cpu": ["x64"] - }, "@esbuild/freebsd-x64@0.25.9": { "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", "os": ["freebsd"], "cpu": ["x64"] }, - "@esbuild/linux-arm64@0.25.7": { - "integrity": "sha512-HfQZQqrNOfS1Okn7PcsGUqHymL1cWGBslf78dGvtrj8q7cN3FkapFgNA4l/a5lXDwr7BqP2BSO6mz9UremNPbg==", - "os": ["linux"], - "cpu": ["arm64"] - }, "@esbuild/linux-arm64@0.25.9": { "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", "os": ["linux"], "cpu": ["arm64"] }, - "@esbuild/linux-arm@0.25.7": { - "integrity": "sha512-JZMIci/1m5vfQuhKoFXogCKVYVfYQmoZJg8vSIMR4TUXbF+0aNlfXH3DGFEFMElT8hOTUF5hisdZhnrZO/bkDw==", - "os": ["linux"], - "cpu": ["arm"] - }, "@esbuild/linux-arm@0.25.9": { "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", "os": ["linux"], "cpu": ["arm"] }, - "@esbuild/linux-ia32@0.25.7": { - "integrity": "sha512-9Jex4uVpdeofiDxnwHRgen+j6398JlX4/6SCbbEFEXN7oMO2p0ueLN+e+9DdsdPLUdqns607HmzEFnxwr7+5wQ==", - "os": ["linux"], - "cpu": ["ia32"] - }, "@esbuild/linux-ia32@0.25.9": { "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", "os": ["linux"], "cpu": ["ia32"] }, - "@esbuild/linux-loong64@0.25.7": { - "integrity": "sha512-TG1KJqjBlN9IHQjKVUYDB0/mUGgokfhhatlay8aZ/MSORMubEvj/J1CL8YGY4EBcln4z7rKFbsH+HeAv0d471w==", - "os": ["linux"], - "cpu": ["loong64"] - }, "@esbuild/linux-loong64@0.25.9": { "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", "os": ["linux"], "cpu": ["loong64"] }, - "@esbuild/linux-mips64el@0.25.7": { - "integrity": "sha512-Ty9Hj/lx7ikTnhOfaP7ipEm/ICcBv94i/6/WDg0OZ3BPBHhChsUbQancoWYSO0WNkEiSW5Do4febTTy4x1qYQQ==", - "os": ["linux"], - "cpu": ["mips64el"] - }, "@esbuild/linux-mips64el@0.25.9": { "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", "os": ["linux"], "cpu": ["mips64el"] }, - "@esbuild/linux-ppc64@0.25.7": { - "integrity": "sha512-MrOjirGQWGReJl3BNQ58BLhUBPpWABnKrnq8Q/vZWWwAB1wuLXOIxS2JQ1LT3+5T+3jfPh0tyf5CpbyQHqnWIQ==", - "os": ["linux"], - "cpu": ["ppc64"] - }, "@esbuild/linux-ppc64@0.25.9": { "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", "os": ["linux"], "cpu": ["ppc64"] }, - "@esbuild/linux-riscv64@0.25.7": { - "integrity": "sha512-9pr23/pqzyqIZEZmQXnFyqp3vpa+KBk5TotfkzGMqpw089PGm0AIowkUppHB9derQzqniGn3wVXgck19+oqiOw==", - "os": ["linux"], - "cpu": ["riscv64"] - }, "@esbuild/linux-riscv64@0.25.9": { "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", "os": ["linux"], "cpu": ["riscv64"] }, - "@esbuild/linux-s390x@0.25.7": { - "integrity": "sha512-4dP11UVGh9O6Y47m8YvW8eoA3r8qL2toVZUbBKyGta8j6zdw1cn9F/Rt59/Mhv0OgY68pHIMjGXWOUaykCnx+w==", - "os": ["linux"], - "cpu": ["s390x"] - }, "@esbuild/linux-s390x@0.25.9": { "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", "os": ["linux"], "cpu": ["s390x"] }, - "@esbuild/linux-x64@0.25.7": { - "integrity": "sha512-ghJMAJTdw/0uhz7e7YnpdX1xVn7VqA0GrWrAO2qKMuqbvgHT2VZiBv1BQ//VcHsPir4wsL3P2oPggfKPzTKoCA==", - "os": ["linux"], - "cpu": ["x64"] - }, "@esbuild/linux-x64@0.25.9": { "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", "os": ["linux"], "cpu": ["x64"] }, - "@esbuild/netbsd-arm64@0.25.7": { - "integrity": "sha512-bwXGEU4ua45+u5Ci/a55B85KWaDSRS8NPOHtxy2e3etDjbz23wlry37Ffzapz69JAGGc4089TBo+dGzydQmydg==", - "os": ["netbsd"], - "cpu": ["arm64"] - }, "@esbuild/netbsd-arm64@0.25.9": { "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", "os": ["netbsd"], "cpu": ["arm64"] }, - "@esbuild/netbsd-x64@0.25.7": { - "integrity": "sha512-tUZRvLtgLE5OyN46sPSYlgmHoBS5bx2URSrgZdW1L1teWPYVmXh+QN/sKDqkzBo/IHGcKcHLKDhBeVVkO7teEA==", - "os": ["netbsd"], - "cpu": ["x64"] - }, "@esbuild/netbsd-x64@0.25.9": { "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", "os": ["netbsd"], "cpu": ["x64"] }, - "@esbuild/openbsd-arm64@0.25.7": { - "integrity": "sha512-bTJ50aoC+WDlDGBReWYiObpYvQfMjBNlKztqoNUL0iUkYtwLkBQQeEsTq/I1KyjsKA5tyov6VZaPb8UdD6ci6Q==", - "os": ["openbsd"], - "cpu": ["arm64"] - }, "@esbuild/openbsd-arm64@0.25.9": { "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", "os": ["openbsd"], "cpu": ["arm64"] }, - "@esbuild/openbsd-x64@0.25.7": { - "integrity": "sha512-TA9XfJrgzAipFUU895jd9j2SyDh9bbNkK2I0gHcvqb/o84UeQkBpi/XmYX3cO1q/9hZokdcDqQxIi6uLVrikxg==", - "os": ["openbsd"], - "cpu": ["x64"] - }, "@esbuild/openbsd-x64@0.25.9": { "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", "os": ["openbsd"], "cpu": ["x64"] }, - "@esbuild/openharmony-arm64@0.25.7": { - "integrity": "sha512-5VTtExUrWwHHEUZ/N+rPlHDwVFQ5aME7vRJES8+iQ0xC/bMYckfJ0l2n3yGIfRoXcK/wq4oXSItZAz5wslTKGw==", - "os": ["openharmony"], - "cpu": ["arm64"] - }, "@esbuild/openharmony-arm64@0.25.9": { "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", "os": ["openharmony"], "cpu": ["arm64"] }, - "@esbuild/sunos-x64@0.25.7": { - "integrity": "sha512-umkbn7KTxsexhv2vuuJmj9kggd4AEtL32KodkJgfhNOHMPtQ55RexsaSrMb+0+jp9XL4I4o2y91PZauVN4cH3A==", - "os": ["sunos"], - "cpu": ["x64"] - }, "@esbuild/sunos-x64@0.25.9": { "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", "os": ["sunos"], "cpu": ["x64"] }, - "@esbuild/win32-arm64@0.25.7": { - "integrity": "sha512-j20JQGP/gz8QDgzl5No5Gr4F6hurAZvtkFxAKhiv2X49yi/ih8ECK4Y35YnjlMogSKJk931iNMcd35BtZ4ghfw==", - "os": ["win32"], - "cpu": ["arm64"] - }, "@esbuild/win32-arm64@0.25.9": { "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", "os": ["win32"], "cpu": ["arm64"] }, - "@esbuild/win32-ia32@0.25.7": { - "integrity": "sha512-4qZ6NUfoiiKZfLAXRsvFkA0hoWVM+1y2bSHXHkpdLAs/+r0LgwqYohmfZCi985c6JWHhiXP30mgZawn/XrqAkQ==", - "os": ["win32"], - "cpu": ["ia32"] - }, "@esbuild/win32-ia32@0.25.9": { "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", "os": ["win32"], "cpu": ["ia32"] }, - "@esbuild/win32-x64@0.25.7": { - "integrity": "sha512-FaPsAHTwm+1Gfvn37Eg3E5HIpfR3i6x1AIcla/MkqAIupD4BW3MrSeUqfoTzwwJhk3WE2/KqUn4/eenEJC76VA==", - "os": ["win32"], - "cpu": ["x64"] - }, "@esbuild/win32-x64@0.25.9": { "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", "os": ["win32"], @@ -2128,32 +1999,32 @@ "esbuild@0.25.9": { "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", "optionalDependencies": [ - "@esbuild/aix-ppc64@0.25.9", - "@esbuild/android-arm@0.25.9", - "@esbuild/android-arm64@0.25.9", - "@esbuild/android-x64@0.25.9", - "@esbuild/darwin-arm64@0.25.9", - "@esbuild/darwin-x64@0.25.9", - "@esbuild/freebsd-arm64@0.25.9", - "@esbuild/freebsd-x64@0.25.9", - "@esbuild/linux-arm@0.25.9", - "@esbuild/linux-arm64@0.25.9", - "@esbuild/linux-ia32@0.25.9", - "@esbuild/linux-loong64@0.25.9", - "@esbuild/linux-mips64el@0.25.9", - "@esbuild/linux-ppc64@0.25.9", - "@esbuild/linux-riscv64@0.25.9", - "@esbuild/linux-s390x@0.25.9", - "@esbuild/linux-x64@0.25.9", - "@esbuild/netbsd-arm64@0.25.9", - "@esbuild/netbsd-x64@0.25.9", - "@esbuild/openbsd-arm64@0.25.9", - "@esbuild/openbsd-x64@0.25.9", - "@esbuild/openharmony-arm64@0.25.9", - "@esbuild/sunos-x64@0.25.9", - "@esbuild/win32-arm64@0.25.9", - "@esbuild/win32-ia32@0.25.9", - "@esbuild/win32-x64@0.25.9" + "@esbuild/aix-ppc64", + "@esbuild/android-arm", + "@esbuild/android-arm64", + "@esbuild/android-x64", + "@esbuild/darwin-arm64", + "@esbuild/darwin-x64", + "@esbuild/freebsd-arm64", + "@esbuild/freebsd-x64", + "@esbuild/linux-arm", + "@esbuild/linux-arm64", + "@esbuild/linux-ia32", + "@esbuild/linux-loong64", + "@esbuild/linux-mips64el", + "@esbuild/linux-ppc64", + "@esbuild/linux-riscv64", + "@esbuild/linux-s390x", + "@esbuild/linux-x64", + "@esbuild/netbsd-arm64", + "@esbuild/netbsd-x64", + "@esbuild/openbsd-arm64", + "@esbuild/openbsd-x64", + "@esbuild/openharmony-arm64", + "@esbuild/sunos-x64", + "@esbuild/win32-arm64", + "@esbuild/win32-ia32", + "@esbuild/win32-x64" ], "scripts": true, "bin": true @@ -3650,6 +3521,9 @@ "wrappy@1.0.2": { "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "ws@8.18.3": { + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==" + }, "y18n@4.0.3": { "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" }, @@ -3835,6 +3709,11 @@ }, "workspace": { "dependencies": [ + "npm:@codemirror/basic-setup@0.20", + "npm:@codemirror/lang-markdown@^6.3.4", + "npm:@codemirror/state@^6.5.2", + "npm:@codemirror/theme-one-dark@^6.1.3", + "npm:@codemirror/view@^6.38.1", "npm:@noble/curves@^1.9.4", "npm:@noble/hashes@^1.8.0", "npm:@nostr-dev-kit/ndk-cache-dexie@2.6", @@ -3845,6 +3724,7 @@ "npm:@tailwindcss/typography@0.5", "npm:asciidoctor@3.0", "npm:bech32@2", + "npm:codemirror@^6.0.2", "npm:d3@^7.9.0", "npm:flowbite-svelte-icons@2.1", "npm:flowbite-svelte@0.48", @@ -3918,6 +3798,7 @@ "npm:typescript@^5.8.3", "npm:vite@^6.3.5", "npm:vitest@^3.1.3", + "npm:ws@^8.18.3", "npm:yaml@^2.5.0" ] } diff --git a/doc/compose_tree.md b/doc/compose_tree.md index 4fa14b5..aa672ab 100644 --- a/doc/compose_tree.md +++ b/doc/compose_tree.md @@ -2,33 +2,42 @@ ## Overview -This document outlines the complete restart plan for implementing NKBIP-01 compliant hierarchical AsciiDoc parsing using proper Asciidoctor tree processor extensions. +This document outlines the complete restart plan for implementing NKBIP-01 +compliant hierarchical AsciiDoc parsing using proper Asciidoctor tree processor +extensions. ## Current State Analysis ### Problems Identified + 1. **Dual Architecture Conflict**: Two competing parsing implementations exist: - `publication_tree_factory.ts` - AST-first approach (currently used) - `publication_tree_extension.ts` - Extension approach (incomplete) -2. **Missing Proper Extension Registration**: Current code doesn't follow the official Asciidoctor extension pattern you provided +2. **Missing Proper Extension Registration**: Current code doesn't follow the + official Asciidoctor extension pattern you provided -3. **Incomplete NKBIP-01 Compliance**: Testing with `deep_hierarchy_test.adoc` may not produce the exact structures shown in `docreference.md` +3. **Incomplete NKBIP-01 Compliance**: Testing with `deep_hierarchy_test.adoc` + may not produce the exact structures shown in `docreference.md` ## NKBIP-01 Specification Summary From `test_data/AsciidocFiles/docreference.md`: ### Event Types + - **30040**: Index events (collections/hierarchical containers) - **30041**: Content events (actual article sections) ### Parse Level Behaviors -- **Level 2**: Only `==` sections → 30041 events (subsections included in content) -- **Level 3**: `==` → 30040 indices, `===` → 30041 content events + +- **Level 2**: Only `==` sections → 30041 events (subsections included in + content) +- **Level 3**: `==` → 30040 indices, `===` → 30041 content events - **Level 4+**: Full hierarchy with each level becoming separate events ### Key Rules + 1. If a section has subsections at target level → becomes 30040 index 2. If no subsections at target level → becomes 30041 content event 3. Content inclusion: 30041 events include all content below parse level @@ -44,13 +53,13 @@ Following the pattern you provided: // Extension registration pattern module.exports = function (registry) { registry.treeProcessor(function () { - var self = this + var self = this; self.process(function (doc) { // Process document and build PublicationTree - return doc - }) - }) -} + return doc; + }); + }); +}; ``` ### Implementation Components @@ -80,11 +89,12 @@ export function registerPublicationTreeProcessor( registry: Registry, ndk: NDK, parseLevel: number, - options?: ProcessorOptions -): { getResult: () => ProcessorResult | null } + options?: ProcessorOptions, +): { getResult: () => ProcessorResult | null }; ``` **Key Features:** + - Follows Asciidoctor extension pattern exactly - Builds events during AST traversal (not after) - Preserves original AsciiDoc content in events @@ -97,11 +107,12 @@ export function registerPublicationTreeProcessor( export async function parseAsciiDocWithTree( content: string, ndk: NDK, - parseLevel: number = 2 -): Promise + parseLevel: number = 2, +): Promise; ``` **Responsibilities:** + - Create Asciidoctor instance - Register tree processor extension - Execute parsing with extension @@ -111,6 +122,7 @@ export async function parseAsciiDocWithTree( ### Phase 3: ZettelEditor Integration **Changes to `ZettelEditor.svelte`:** + - Replace `createPublicationTreeFromContent()` calls - Use new `parseAsciiDocWithTree()` function - Maintain existing preview/publishing interface @@ -119,6 +131,7 @@ export async function parseAsciiDocWithTree( ### Phase 4: Validation Testing **Test Suite:** + 1. Parse `deep_hierarchy_test.adoc` at levels 2-7 2. Verify event structures match `docreference.md` examples 3. Validate content preservation and tag inheritance @@ -127,23 +140,29 @@ export async function parseAsciiDocWithTree( ## File Organization ### Files to Create + 1. `src/lib/utils/publication_tree_processor.ts` - Core tree processor extension 2. `src/lib/utils/asciidoc_publication_parser.ts` - Unified parser interface 3. `tests/unit/publication_tree_processor.test.ts` - Comprehensive test suite ### Files to Modify + 1. `src/lib/components/ZettelEditor.svelte` - Update parsing calls 2. `src/routes/new/compose/+page.svelte` - Verify integration works ### Files to Remove (After Validation) + 1. `src/lib/utils/publication_tree_factory.ts` - Replace with processor 2. `src/lib/utils/publication_tree_extension.ts` - Merge concepts into processor ## Success Criteria -1. **NKBIP-01 Compliance**: All parse levels produce structures exactly matching `docreference.md` -2. **Content Preservation**: Original AsciiDoc content preserved in events (not converted to HTML) -3. **Proper Extension Pattern**: Uses official Asciidoctor tree processor registration +1. **NKBIP-01 Compliance**: All parse levels produce structures exactly matching + `docreference.md` +2. **Content Preservation**: Original AsciiDoc content preserved in events (not + converted to HTML) +3. **Proper Extension Pattern**: Uses official Asciidoctor tree processor + registration 4. **Zero Regression**: Current ZettelEditor functionality unchanged 5. **Performance**: No degradation in parsing or preview speed 6. **Test Coverage**: Comprehensive validation with `deep_hierarchy_test.adoc` @@ -152,7 +171,7 @@ export async function parseAsciiDocWithTree( 1. **Study & Plan** ✓ (Current phase) 2. **Implement Core Processor** - Create `publication_tree_processor.ts` -3. **Build Unified Interface** - Create `asciidoc_publication_parser.ts` +3. **Build Unified Interface** - Create `asciidoc_publication_parser.ts` 4. **Integrate with ZettelEditor** - Update parsing calls 5. **Validate with Test Documents** - Verify NKBIP-01 compliance 6. **Clean Up Legacy Code** - Remove old implementations @@ -169,5 +188,6 @@ export async function parseAsciiDocWithTree( - NKBIP-01 Specification: `test_data/AsciidocFiles/docreference.md` - Test Document: `test_data/AsciidocFiles/deep_hierarchy_test.adoc` -- Asciidoctor Extensions: [Official Documentation](https://docs.asciidoctor.org/asciidoctor.js/latest/extend/extensions/) -- Current Implementation: `src/lib/components/ZettelEditor.svelte:64` \ No newline at end of file +- Asciidoctor Extensions: + [Official Documentation](https://docs.asciidoctor.org/asciidoctor.js/latest/extend/extensions/) +- Current Implementation: `src/lib/components/ZettelEditor.svelte:64` diff --git a/import_map.json b/import_map.json index f5bfb21..ff51d8c 100644 --- a/import_map.json +++ b/import_map.json @@ -1,5 +1,10 @@ { "imports": { + "@codemirror/basic-setup": "npm:@codemirror/basic-setup@^0.20.0", + "@codemirror/lang-markdown": "npm:@codemirror/lang-markdown@^6.3.4", + "@codemirror/state": "npm:@codemirror/state@^6.5.2", + "@codemirror/theme-one-dark": "npm:@codemirror/theme-one-dark@^6.1.3", + "@codemirror/view": "npm:@codemirror/view@^6.38.1", "he": "npm:he@1.2.x", "@nostr-dev-kit/ndk": "npm:@nostr-dev-kit/ndk@^2.14.32", "@nostr-dev-kit/ndk-cache-dexie": "npm:@nostr-dev-kit/ndk-cache-dexie@2.6.x", @@ -8,6 +13,7 @@ "@tailwindcss/postcss": "npm:@tailwindcss/postcss@^4.1.11", "@tailwindcss/typography": "npm:@tailwindcss/typography@0.5.x", "asciidoctor": "npm:asciidoctor@3.0.x", + "codemirror": "npm:codemirror@^6.0.2", "d3": "npm:d3@^7.9.0", "nostr-tools": "npm:nostr-tools@2.15.x", "tailwind-merge": "npm:tailwind-merge@^3.3.1", diff --git a/nips/09.md b/nips/09.md index 23ffeab..b297b38 100644 --- a/nips/09.md +++ b/nips/09.md @@ -1,14 +1,16 @@ -NIP-09 -====== +# NIP-09 -Event Deletion Request ----------------------- +## Event Deletion Request `draft` `optional` -A special event with kind `5`, meaning "deletion request" is defined as having a list of one or more `e` or `a` tags, each referencing an event the author is requesting to be deleted. Deletion requests SHOULD include a `k` tag for the kind of each event being requested for deletion. +A special event with kind `5`, meaning "deletion request" is defined as having a +list of one or more `e` or `a` tags, each referencing an event the author is +requesting to be deleted. Deletion requests SHOULD include a `k` tag for the +kind of each event being requested for deletion. -The event's `content` field MAY contain a text note describing the reason for the deletion request. +The event's `content` field MAY contain a text note describing the reason for +the deletion request. For example: @@ -28,26 +30,48 @@ For example: } ``` -Relays SHOULD delete or stop publishing any referenced events that have an identical `pubkey` as the deletion request. Clients SHOULD hide or otherwise indicate a deletion request status for referenced events. +Relays SHOULD delete or stop publishing any referenced events that have an +identical `pubkey` as the deletion request. Clients SHOULD hide or otherwise +indicate a deletion request status for referenced events. -Relays SHOULD continue to publish/share the deletion request events indefinitely, as clients may already have the event that's intended to be deleted. Additionally, clients SHOULD broadcast deletion request events to other relays which don't have it. +Relays SHOULD continue to publish/share the deletion request events +indefinitely, as clients may already have the event that's intended to be +deleted. Additionally, clients SHOULD broadcast deletion request events to other +relays which don't have it. -When an `a` tag is used, relays SHOULD delete all versions of the replaceable event up to the `created_at` timestamp of the deletion request event. +When an `a` tag is used, relays SHOULD delete all versions of the replaceable +event up to the `created_at` timestamp of the deletion request event. ## Client Usage -Clients MAY choose to fully hide any events that are referenced by valid deletion request events. This includes text notes, direct messages, or other yet-to-be defined event kinds. Alternatively, they MAY show the event along with an icon or other indication that the author has "disowned" the event. The `content` field MAY also be used to replace the deleted events' own content, although a user interface should clearly indicate that this is a deletion request reason, not the original content. +Clients MAY choose to fully hide any events that are referenced by valid +deletion request events. This includes text notes, direct messages, or other +yet-to-be defined event kinds. Alternatively, they MAY show the event along with +an icon or other indication that the author has "disowned" the event. The +`content` field MAY also be used to replace the deleted events' own content, +although a user interface should clearly indicate that this is a deletion +request reason, not the original content. -A client MUST validate that each event `pubkey` referenced in the `e` tag of the deletion request is identical to the deletion request `pubkey`, before hiding or deleting any event. Relays can not, in general, perform this validation and should not be treated as authoritative. +A client MUST validate that each event `pubkey` referenced in the `e` tag of the +deletion request is identical to the deletion request `pubkey`, before hiding or +deleting any event. Relays can not, in general, perform this validation and +should not be treated as authoritative. -Clients display the deletion request event itself in any way they choose, e.g., not at all, or with a prominent notice. +Clients display the deletion request event itself in any way they choose, e.g., +not at all, or with a prominent notice. -Clients MAY choose to inform the user that their request for deletion does not guarantee deletion because it is impossible to delete events from all relays and clients. +Clients MAY choose to inform the user that their request for deletion does not +guarantee deletion because it is impossible to delete events from all relays and +clients. ## Relay Usage -Relays MAY validate that a deletion request event only references events that have the same `pubkey` as the deletion request itself, however this is not required since relays may not have knowledge of all referenced events. +Relays MAY validate that a deletion request event only references events that +have the same `pubkey` as the deletion request itself, however this is not +required since relays may not have knowledge of all referenced events. ## Deletion Request of a Deletion Request -Publishing a deletion request event against a deletion request has no effect. Clients and relays are not obliged to support "unrequest deletion" functionality. +Publishing a deletion request event against a deletion request has no effect. +Clients and relays are not obliged to support "unrequest deletion" +functionality. diff --git a/package-lock.json b/package-lock.json index 6ae7ca4..8d6981a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -107,7 +107,6 @@ "resolved": "https://registry.npmjs.org/@asciidoctor/core/-/core-3.0.4.tgz", "integrity": "sha512-41SDMi7iRRBViPe0L6VWFTe55bv6HEOJeRqMj5+E5wB1YPdUPuTucL4UAESPZM6OWmn4t/5qM5LusXomFUVwVQ==", "license": "MIT", - "peer": true, "dependencies": { "@asciidoctor/opal-runtime": "3.0.1", "unxhr": "1.2.0" @@ -1176,6 +1175,7 @@ "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } @@ -1186,6 +1186,7 @@ "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", @@ -1201,6 +1202,7 @@ "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", "dev": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } @@ -1211,6 +1213,7 @@ "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.15" }, @@ -1224,6 +1227,7 @@ "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -1248,6 +1252,7 @@ "integrity": "sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1261,6 +1266,7 @@ "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } @@ -1271,6 +1277,7 @@ "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@eslint/core": "^0.15.2", "levn": "^0.4.1" @@ -1313,6 +1320,7 @@ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", "dev": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=18.18.0" } @@ -1323,6 +1331,7 @@ "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" @@ -1337,6 +1346,7 @@ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=12.22" }, @@ -1351,6 +1361,7 @@ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=18.18" }, @@ -2262,7 +2273,6 @@ "integrity": "sha512-H8eXW5TSziSvt9d5IJ5pPyWGhXQLdmq+17H9j7aofA/TsfSvG8ZIpTjObphFRNagfIyoFGyoB3lOzdsGHKiKpw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", @@ -2302,7 +2312,6 @@ "integrity": "sha512-nJsV36+o7rZUDlrnSduMNl11+RoDE1cKqOI0yUEBCcqFoAZOk47TwD3dPKS2WmRutke9StXnzsPBslY7prDM9w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "debug": "^4.4.1", @@ -2365,7 +2374,6 @@ "integrity": "sha512-/VNHWYhNu+BS7ktbYoVGrCmsXDh+chFMaONMwGNdIBcFHrWqk2jY8fNyr3DLdtQUIalvkPfM554ZSFa3dm3nxQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/Fuzzyma" @@ -2391,7 +2399,6 @@ "integrity": "sha512-qkMgso1sd2hXKd1FZ1weO7ANq12sNmQJeGDjs46QwDVsxSRcHmvWKL2NDF7Yimpwf3sl5esOLkPqtV2bQ3v/Jg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 14.18" }, @@ -3012,7 +3019,8 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/mathjax": { "version": "0.0.40", @@ -3191,7 +3199,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3215,6 +3222,7 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -3256,6 +3264,7 @@ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "license": "ISC", "optional": true, + "peer": true, "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -3270,6 +3279,7 @@ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=8.6" }, @@ -3297,7 +3307,8 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, - "license": "Python-2.0" + "license": "Python-2.0", + "peer": true }, "node_modules/aria-query": { "version": "5.3.2", @@ -3445,6 +3456,7 @@ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=8" }, @@ -3458,6 +3470,7 @@ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3469,6 +3482,7 @@ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "fill-range": "^7.1.1" }, @@ -3496,7 +3510,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", @@ -3556,6 +3569,7 @@ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6" } @@ -3613,6 +3627,7 @@ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -3658,6 +3673,7 @@ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -3683,6 +3699,7 @@ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "license": "ISC", "optional": true, + "peer": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -3860,7 +3877,8 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/constantinople": { "version": "4.0.1", @@ -3894,6 +3912,7 @@ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -4232,7 +4251,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -4369,7 +4387,8 @@ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/deepmerge": { "version": "4.3.1", @@ -4579,6 +4598,7 @@ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -4805,6 +4825,7 @@ "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "dependencies": { "estraverse": "^5.1.0" }, @@ -4876,21 +4897,24 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/fdir": { "version": "6.5.0", @@ -4916,6 +4940,7 @@ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "flat-cache": "^4.0.0" }, @@ -4959,6 +4984,7 @@ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -4972,6 +4998,7 @@ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -4989,6 +5016,7 @@ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" @@ -5002,7 +5030,8 @@ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true, - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/flowbite": { "version": "2.5.2", @@ -5224,6 +5253,7 @@ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -5258,6 +5288,7 @@ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -5310,6 +5341,7 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -5389,6 +5421,7 @@ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 4" } @@ -5399,6 +5432,7 @@ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -5416,6 +5450,7 @@ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.8.19" } @@ -5452,6 +5487,7 @@ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "binary-extensions": "^2.0.0" }, @@ -5502,6 +5538,7 @@ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "devOptional": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -5521,6 +5558,7 @@ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -5541,6 +5579,7 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=0.12.0" } @@ -5584,7 +5623,8 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/jake": { "version": "10.9.4", @@ -5631,6 +5671,7 @@ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "argparse": "^2.0.1" }, @@ -5643,21 +5684,24 @@ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/jstransformer": { "version": "1.0.0", @@ -5675,6 +5719,7 @@ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "json-buffer": "3.0.1" } @@ -5702,6 +5747,7 @@ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -6005,6 +6051,7 @@ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "p-locate": "^5.0.0" }, @@ -6076,6 +6123,7 @@ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -6141,7 +6189,8 @@ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/neo-async": { "version": "2.6.2", @@ -6177,6 +6226,7 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6196,7 +6246,6 @@ "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.15.2.tgz", "integrity": "sha512-utmqVVS4HMDiwhIgI6Cr+KqA4aUhF3Sb755iO/qCiqxc5H9JW/9Z3N1RO/jKWpjP6q/Vx0lru7IYuiPvk+2/ng==", "license": "Unlicense", - "peer": true, "dependencies": { "@noble/ciphers": "^0.5.1", "@noble/curves": "1.2.0", @@ -6327,6 +6376,7 @@ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -6345,6 +6395,7 @@ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "yocto-queue": "^0.1.0" }, @@ -6361,6 +6412,7 @@ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "p-limit": "^3.0.2" }, @@ -6386,6 +6438,7 @@ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "callsites": "^3.0.0" }, @@ -6408,6 +6461,7 @@ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -6545,7 +6599,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -6696,6 +6749,7 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8.0" } @@ -6706,7 +6760,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -6867,6 +6920,7 @@ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6" } @@ -7022,6 +7076,7 @@ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "picomatch": "^2.2.1" }, @@ -7035,6 +7090,7 @@ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=8.6" }, @@ -7083,6 +7139,7 @@ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=4" } @@ -7099,7 +7156,6 @@ "integrity": "sha512-+IuescNkTJQgX7AkIDtITipZdIGcWF0pnVvZTWStiazUmcGA2ag8dfg0urest2XlXUi9kuhfQ+qmdc5Stc3z7g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -7193,6 +7249,7 @@ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -7206,6 +7263,7 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -7308,6 +7366,7 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" }, @@ -7340,6 +7399,7 @@ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -7364,7 +7424,6 @@ "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.39.4.tgz", "integrity": "sha512-VU729KzEau1l6d6d25EnRQhdkwwYdTQxQrF8gdUfjZ3dCjrG7VmRMylMxx92ayO9/z5PKWpDrShJdzc4PGW1uA==", "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", @@ -7497,7 +7556,6 @@ "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", "dev": true, "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/dcastil" @@ -7527,8 +7585,7 @@ "version": "4.1.16", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz", "integrity": "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tapable": { "version": "2.3.0", @@ -7610,6 +7667,7 @@ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "is-number": "^7.0.0" }, @@ -7652,6 +7710,7 @@ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "prelude-ls": "^1.2.1" }, @@ -7665,7 +7724,6 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7755,6 +7813,7 @@ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "punycode": "^2.1.0" } @@ -7771,7 +7830,6 @@ "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -7978,6 +8036,7 @@ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "isexe": "^2.0.0" }, @@ -8032,6 +8091,7 @@ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -8102,7 +8162,6 @@ "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "dev": true, "license": "ISC", - "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -8143,6 +8202,7 @@ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, diff --git a/src/lib/a/cards/AEventPreview.svelte b/src/lib/a/cards/AEventPreview.svelte index 52ac27f..e8bf925 100644 --- a/src/lib/a/cards/AEventPreview.svelte +++ b/src/lib/a/cards/AEventPreview.svelte @@ -1,77 +1,77 @@ @@ -84,15 +90,21 @@ {#each $visualizationConfig.eventConfigs as ec} {@const isEnabled = ec.enabled !== false} {@const isLoaded = (eventCounts[ec.kind] || 0) > 0} - {@const borderColor = isLoaded ? 'border-green-500' : 'border-red-500'} + {@const borderColor = isLoaded ? "border-green-500" : "border-red-500"} {/each} - + {#if !showAddInput} - {/if} - - - + {#if showAddInput}
- - + - -
+ {#snippet footer()} +
+ + +
+ {/snippet} {:else} @@ -335,10 +337,9 @@ {#if isEditing} - toggleEditing(rootId, false)} - /> + {#snippet right()} + toggleEditing(rootId, false)} /> + {/snippet} @@ -727,14 +868,18 @@ {#if isExpanded}
{#each sortedHighlights as highlight} - {@const truncated = useMockHighlights ? "test data" : truncateHighlight(highlight.content)} + {@const truncated = useMockHighlights + ? "test data" + : truncateHighlight(highlight.content)} {@const showCopied = copyFeedback === highlight.id}
@@ -744,12 +889,30 @@ title="Copy naddr" > {#if showCopied} - - + + {:else} - - + + {/if} @@ -776,8 +939,9 @@ animation: flash 1.5s ease-in-out; } - @keyframes :global(flash) { - 0%, 100% { + @keyframes -global-flash { + 0%, + 100% { filter: brightness(1); } 50% { diff --git a/src/lib/components/publications/HighlightSelectionHandler.svelte b/src/lib/components/publications/HighlightSelectionHandler.svelte index 1a17986..3e2efd6 100644 --- a/src/lib/components/publications/HighlightSelectionHandler.svelte +++ b/src/lib/components/publications/HighlightSelectionHandler.svelte @@ -73,7 +73,7 @@ tags: tags, content: selectedText, id: "", - sig: "" + sig: "", }; }); @@ -110,7 +110,7 @@ address: sectionAddress, eventId: sectionEventId, allDataAttrs: publicationSection.dataset, - sectionId: publicationSection.id + sectionId: publicationSection.id, }); currentSelection = selection; @@ -151,13 +151,14 @@ event.pubkey = $userStore.pubkey; // Set pubkey from user store // Use the specific section's address/ID if available, otherwise fall back to publication event - const useAddress = selectedSectionAddress || publicationEvent.tagAddress(); + const useAddress = + selectedSectionAddress || publicationEvent.tagAddress(); const useEventId = selectedSectionEventId || publicationEvent.id; console.log("[HighlightSelectionHandler] Creating highlight with:", { address: useAddress, eventId: useEventId, - fallbackUsed: !selectedSectionAddress + fallbackUsed: !selectedSectionAddress, }); const tags: string[][] = []; @@ -202,7 +203,11 @@ content: String(event.content), }; - if (typeof window !== "undefined" && window.nostr && window.nostr.signEvent) { + if ( + typeof window !== "undefined" && + window.nostr && + window.nostr.signEvent + ) { const signed = await window.nostr.signEvent(plainEvent); event.sig = signed.sig; if ("id" in signed) { @@ -222,7 +227,10 @@ // Remove duplicates const uniqueRelays = Array.from(new Set(relays)); - console.log("[HighlightSelectionHandler] Publishing to relays:", uniqueRelays); + console.log( + "[HighlightSelectionHandler] Publishing to relays:", + uniqueRelays, + ); const signedEvent = { ...plainEvent, @@ -248,11 +256,15 @@ clearTimeout(timeout); if (ok) { publishedCount++; - console.log(`[HighlightSelectionHandler] Published to ${relayUrl}`); + console.log( + `[HighlightSelectionHandler] Published to ${relayUrl}`, + ); WebSocketPool.instance.release(ws); resolve(); } else { - console.warn(`[HighlightSelectionHandler] ${relayUrl} rejected: ${message}`); + console.warn( + `[HighlightSelectionHandler] ${relayUrl} rejected: ${message}`, + ); WebSocketPool.instance.release(ws); reject(new Error(message)); } @@ -263,7 +275,10 @@ ws.send(JSON.stringify(["EVENT", signedEvent])); }); } catch (e) { - console.error(`[HighlightSelectionHandler] Failed to publish to ${relayUrl}:`, e); + console.error( + `[HighlightSelectionHandler] Failed to publish to ${relayUrl}:`, + e, + ); } } @@ -271,7 +286,10 @@ throw new Error("Failed to publish to any relays"); } - showFeedbackMessage(`Highlight created and published to ${publishedCount} relay(s)!`, "success"); + showFeedbackMessage( + `Highlight created and published to ${publishedCount} relay(s)!`, + "success", + ); // Clear the selection if (currentSelection) { @@ -294,7 +312,10 @@ } } catch (error) { console.error("Failed to create highlight:", error); - showFeedbackMessage("Failed to create highlight. Please try again.", "error"); + showFeedbackMessage( + "Failed to create highlight. Please try again.", + "error", + ); } finally { isSubmitting = false; } @@ -349,11 +370,18 @@ {#if showConfirmModal} - +

Selected Text:

-
+

"{selectedText}"

@@ -366,16 +394,21 @@ id="comment" bind:value={comment} placeholder="Share your thoughts about this highlight..." - rows="3" + rows={3} class="w-full" />
{#if showJsonPreview && previewJson} -
+

Event JSON Preview:

-
{JSON.stringify(previewJson, null, 2)}
+
{JSON.stringify(previewJson, null, 2)}
{/if} @@ -383,7 +416,7 @@
- -
@@ -409,7 +450,9 @@ {#if showFeedback}
diff --git a/src/lib/components/publications/Publication.svelte b/src/lib/components/publications/Publication.svelte index 422abf6..fdfc7b1 100644 --- a/src/lib/components/publications/Publication.svelte +++ b/src/lib/components/publications/Publication.svelte @@ -7,7 +7,8 @@ SidebarGroup, SidebarWrapper, Heading, - CloseButton, uiHelpers + CloseButton, + uiHelpers, } from "flowbite-svelte"; import { getContext, onDestroy, onMount } from "svelte"; import { @@ -37,13 +38,14 @@ import { Textarea, P } from "flowbite-svelte"; import { userStore } from "$lib/stores/userStore"; - let { rootAddress, publicationType, indexEvent, publicationTree, toc } = $props<{ - rootAddress: string; - publicationType: string; - indexEvent: NDKEvent; - publicationTree: SveltePublicationTree; - toc: TocType; - }>(); + let { rootAddress, publicationType, indexEvent, publicationTree, toc } = + $props<{ + rootAddress: string; + publicationType: string; + indexEvent: NDKEvent; + publicationTree: SveltePublicationTree; + toc: TocType; + }>(); const ndk = getNdkContext(); @@ -64,23 +66,25 @@ // Toggle between mock and real data for testing (DEBUG MODE) // Can be controlled via VITE_USE_MOCK_COMMENTS and VITE_USE_MOCK_HIGHLIGHTS environment variables - let useMockComments = $state(import.meta.env.VITE_USE_MOCK_COMMENTS === "true"); - let useMockHighlights = $state(import.meta.env.VITE_USE_MOCK_HIGHLIGHTS === "true"); + let useMockComments = $state( + import.meta.env.VITE_USE_MOCK_COMMENTS === "true", + ); + let useMockHighlights = $state( + import.meta.env.VITE_USE_MOCK_HIGHLIGHTS === "true", + ); // Log initial state for debugging - console.log('[Publication] Mock data initialized:', { - useMockComments, - useMockHighlights, + console.log("[Publication] Mock data initialized:", { envVars: { VITE_USE_MOCK_COMMENTS: import.meta.env.VITE_USE_MOCK_COMMENTS, VITE_USE_MOCK_HIGHLIGHTS: import.meta.env.VITE_USE_MOCK_HIGHLIGHTS, - } + }, }); // Derive all event IDs and addresses for highlight fetching let allEventIds = $derived.by(() => { const ids = [indexEvent.id]; - leaves.forEach(leaf => { + leaves.forEach((leaf) => { if (leaf?.id) ids.push(leaf.id); }); return ids; @@ -88,7 +92,7 @@ let allEventAddresses = $derived.by(() => { const addresses = [rootAddress]; - leaves.forEach(leaf => { + leaves.forEach((leaf) => { if (leaf) { const addr = leaf.tagAddress(); if (addr) addresses.push(addr); @@ -99,11 +103,11 @@ // Filter comments for the root publication (kind 30040) let articleComments = $derived( - comments.filter(comment => { + comments.filter((comment) => { // Check if comment targets the root publication via #a tag - const aTag = comment.tags.find(t => t[0] === 'a'); + const aTag = comment.tags.find((t) => t[0] === "a"); return aTag && aTag[1] === rootAddress; - }) + }), ); // #region Loading @@ -124,9 +128,11 @@ console.warn("[Publication] publicationTree is not available"); return; } - - console.log(`[Publication] Loading ${count} more events. Current leaves: ${leaves.length}, loaded addresses: ${loadedAddresses.size}`); - + + console.log( + `[Publication] Loading ${count} more events. Current leaves: ${leaves.length}, loaded addresses: ${loadedAddresses.size}`, + ); + isLoading = true; try { @@ -159,7 +165,9 @@ console.error("[Publication] Error loading more content:", error); } finally { isLoading = false; - console.log(`[Publication] Finished loading. Total leaves: ${leaves.length}, loaded addresses: ${loadedAddresses.size}`); + console.log( + `[Publication] Finished loading. Total leaves: ${leaves.length}, loaded addresses: ${loadedAddresses.size}`, + ); } } @@ -196,12 +204,12 @@ lastElementRef = null; loadedAddresses = new Set(); hasInitialized = false; - + // Reset the publication tree iterator to prevent duplicate events - if (typeof publicationTree.resetIterator === 'function') { + if (typeof publicationTree.resetIterator === "function") { publicationTree.resetIterator(); } - + // AI-NOTE: Use setTimeout to ensure iterator reset completes before loading // This prevents race conditions where loadMore is called before the iterator is fully reset setTimeout(() => { @@ -298,7 +306,9 @@ const kind = parseInt(kindStr); // Create comment event (kind 1111) - const commentEvent = new (await import("@nostr-dev-kit/ndk")).NDKEvent(ndk); + const commentEvent = new (await import("@nostr-dev-kit/ndk")).NDKEvent( + ndk, + ); commentEvent.kind = 1111; commentEvent.content = articleCommentContent; @@ -330,10 +340,10 @@ articleCommentSuccess = false; handleCommentPosted(); }, 1500); - } catch (err) { console.error("[Publication] Error posting article comment:", err); - articleCommentError = err instanceof Error ? err.message : "Failed to post comment"; + articleCommentError = + err instanceof Error ? err.message : "Failed to post comment"; } finally { isSubmittingArticleComment = false; } @@ -344,30 +354,36 @@ */ async function handleDeletePublication() { const confirmed = confirm( - "Are you sure you want to delete this entire publication? This action will publish a deletion request to all relays." + "Are you sure you want to delete this entire publication? This action will publish a deletion request to all relays.", ); if (!confirmed) return; try { - await deleteEvent({ - eventAddress: indexEvent.tagAddress(), - eventKind: indexEvent.kind, - reason: "User deleted publication", - onSuccess: (deletionEventId) => { - console.log("[Publication] Deletion event published:", deletionEventId); - publicationDeleted = true; - - // Redirect after 2 seconds - setTimeout(() => { - goto("/publications"); - }, 2000); + await deleteEvent( + { + eventAddress: indexEvent.tagAddress(), + eventKind: indexEvent.kind, + reason: "User deleted publication", + onSuccess: (deletionEventId) => { + console.log( + "[Publication] Deletion event published:", + deletionEventId, + ); + publicationDeleted = true; + + // Redirect after 2 seconds + setTimeout(() => { + goto("/publications"); + }, 2000); + }, + onError: (error) => { + console.error("[Publication] Failed to delete publication:", error); + alert(`Failed to delete publication: ${error}`); + }, }, - onError: (error) => { - console.error("[Publication] Failed to delete publication:", error); - alert(`Failed to delete publication: ${error}`); - }, - }); + ndk, + ); } catch (error) { console.error("[Publication] Error deleting publication:", error); alert(`Error: ${error}`); @@ -422,14 +438,19 @@ observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { - if (entry.isIntersecting && !isLoading && !isDone && publicationTree) { + if ( + entry.isIntersecting && + !isLoading && + !isDone && + publicationTree + ) { loadMore(1); } }); }, { threshold: 0.5 }, ); - + // AI-NOTE: Removed duplicate loadMore call // Initial content loading is handled by the $effect that watches publicationTree // This prevents duplicate loading when both onMount and $effect trigger @@ -450,14 +471,11 @@ -
+
- - +
-
+
{#if publicationType !== "blog" && !isLeaf} {#if $publicationColumnVisibility.toc} - + publicationTree.setBookmark(address)} + onSectionFocused={(address: string) => + publicationTree.setBookmark(address)} onLoadMore={() => { - if (!isLoading && !isDone && publicationTree) { - loadMore(4); - } - }} + if (!isLoading && !isDone && publicationTree) { + loadMore(4); + } + }} /> - {/if} {/if} -
{#if $publicationColumnVisibility.main} -
+
@@ -521,7 +549,10 @@
-
+
{#if publicationDeleted} @@ -542,7 +573,9 @@
-