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:
@@ -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-<MM/DD/YYYY>:` - 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:
@@ -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
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
@@ -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
// 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) {
@@ -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) {
@@ -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() {
@@ -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
- ✅ Validates invalid address format (too few parts)
@ -17,6 +21,7 @@ Comprehensive test suite for CommentButton component and NIP-22 comment function
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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:
@@ -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"
@ -25,6 +25,7 @@ This syntax automatically generates a 'w' tag during conversion:
@@ -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:
@@ -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:
@@ -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"
@@ -61,6 +65,7 @@ Event with d-tag "knowledge-graphs"
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
// Relays to publish to (matching CommentLayer's relay list)
constrelays=[
'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
consttestComments=[
{
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.",
// and optionally a user comment/annotation in the ["comment", ...] tag
consttestHighlights=[
{
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) {
@@ -81,16 +96,18 @@ async function publishEvent(event, relayUrl) {
constws=newWebSocket(relayUrl);
letpublished=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)=>{
constmessage=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) {
@@ -102,14 +119,14 @@ async function publishEvent(event, relayUrl) {
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:
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:
@@ -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"
// Replace placeholders with actual wiki link HTML
// Use a global regex to catch all occurrences (Asciidoctor might have duplicated them)
placeholders.forEach((link, placeholder) => {
const className =
link.type === "auto"
? "wiki-link wiki-link-auto"
: link.type === "w"
? "wiki-link wiki-link-ref"
: "wiki-link wiki-link-def";
const title =
link.type === "w"
? "Wiki reference (mentions this concept)"
: link.type === "d"
? "Wiki definition (defines this concept)"
: "Wiki link (searches both references and definitions)";
const html = `<aclass="${className}"href="#wiki/${link.type}/${encodeURIComponent(link.term)}"title="${title}"data-wiki-type="${link.type}"data-wiki-term="${link.term}">${link.displayText}</a>`;
// Use global replace to handle all occurrences
const regex = new RegExp(
placeholder.replace(
/[.*+?^${}()|[\]\\]/g,
"\\$&",
),
"g",
);
rendered = rendered
.toString()
.replace(regex, html);
});
}
// Replace placeholders with actual wiki link HTML
// Use a global regex to catch all occurrences (Asciidoctor might have duplicated them)
placeholders.forEach((link, placeholder) => {
const className =
link.type === 'auto'
? 'wiki-link wiki-link-auto'
: link.type === 'w'
? 'wiki-link wiki-link-ref'
: 'wiki-link wiki-link-def';
const title =
link.type === 'w'
? 'Wiki reference (mentions this concept)'
: link.type === 'd'
? 'Wiki definition (defines this concept)'
: 'Wiki link (searches both references and definitions)';
const html = `<aclass="${className}"href="#wiki/${link.type}/${encodeURIComponent(link.term)}"title="${title}"data-wiki-type="${link.type}"data-wiki-term="${link.term}">${link.displayText}</a>`;
// Use global replace to handle all occurrences
const regex = new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g');
console.log(`[HighlightLayer] Searching in specific section: ${targetAddress}`);
console.log(
`[HighlightLayer] Searching in specific section: ${targetAddress}`,
);
} else {
console.log(`[HighlightLayer] Section ${targetAddress} not found in DOM, searching globally`);
console.log(
`[HighlightLayer] Section ${targetAddress} not found in DOM, searching globally`,
);
}
}
console.log(`[HighlightLayer] Searching for text: "${text}" in`, searchRoot);
console.log(
`[HighlightLayer] Searching for text: "${text}" in`,
searchRoot,
);
// Use TreeWalker to find all text nodes
const walker = document.createTreeWalker(
searchRoot,
NodeFilter.SHOW_TEXT,
null
null,
);
const textNodes: Node[] = [];
@ -338,19 +412,30 @@
@@ -338,19 +412,30 @@
}
// Search for the highlight text in text nodes
console.log(`[HighlightLayer] Searching through ${textNodes.length} text nodes`);
console.log(
`[HighlightLayer] Searching through ${textNodes.length} text nodes`,
);
for (const textNode of textNodes) {
const nodeText = textNode.textContent || "";
const index = nodeText.toLowerCase().indexOf(text.toLowerCase());
if (index !== -1) {
console.log(`[HighlightLayer] Found match in text node:`, nodeText.substring(Math.max(0, index - 20), Math.min(nodeText.length, index + text.length + 20)));
console.log(
`[HighlightLayer] Found match in text node:`,
nodeText.substring(
Math.max(0, index - 20),
Math.min(nodeText.length, index + text.length + 20),
),
);
const parent = textNode.parentNode;
if (!parent) continue;
// Skip if already highlighted
if (parent.nodeName === "MARK" || (parent instanceof Element && parent.classList?.contains("highlight"))) {
if (
parent.nodeName === "MARK" ||
(parent instanceof Element && parent.classList?.contains("highlight"))
) {
continue;
}
@ -386,10 +471,14 @@
@@ -386,10 +471,14 @@
* Render all highlights on the page
*/
function renderHighlights() {
console.log(`[HighlightLayer] renderHighlights called - visible: ${visible}, containerRef: ${!!containerRef}, highlights: ${highlights.length}`);
console.log(
`[HighlightLayer] renderHighlights called - visible: ${visible}, containerRef: ${!!containerRef}, highlights: ${highlights.length}`,
// Sample highlighted text snippets (things users might actually highlight)
consthighlightedTexts=[
'Knowledge that tries to stay put inevitably becomes ossified',
'The attempt to hold knowledge still is like trying to photograph a river',
'Understanding emerges not from rigid frameworks but from fluid engagement',
'Traditional institutions struggle with the natural promiscuity of ideas',
'Thinking without permission means refusing predetermined categories',
'The most valuable insights often come from unexpected juxtapositions',
'Anarchistic knowledge rejects the notion of authorized interpreters',
'Every act of reading is an act of creative interpretation',
'Hierarchy in knowledge systems serves power, not understanding',
'The boundary between creator and consumer is an artificial construction',
"Knowledge that tries to stay put inevitably becomes ossified",
"The attempt to hold knowledge still is like trying to photograph a river",
"Understanding emerges not from rigid frameworks but from fluid engagement",
"Traditional institutions struggle with the natural promiscuity of ideas",
"Thinking without permission means refusing predetermined categories",
"The most valuable insights often come from unexpected juxtapositions",
"Anarchistic knowledge rejects the notion of authorized interpreters",
"Every act of reading is an act of creative interpretation",
"Hierarchy in knowledge systems serves power, not understanding",
"The boundary between creator and consumer is an artificial construction",
];
// Context strings (surrounding text to help locate the highlight)
constcontexts=[
'This is the fundamental paradox of institutionalized knowledge. 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. What remains is a static representation, not the dynamic reality.',
'Understanding emerges not from rigid frameworks but from fluid engagement with ideas, people, and contexts. This fluidity is precisely what traditional systems attempt to eliminate.',
'Traditional institutions struggle with the natural promiscuity of ideas—the way concepts naturally migrate, mutate, and merge across boundaries that were meant to contain them.',
'Thinking without permission means refusing predetermined categories and challenging the gatekeepers who claim authority over legitimate thought.',
'The most valuable insights often come from unexpected juxtapositions, from bringing together ideas that were never meant to meet.',
'Anarchistic knowledge rejects the notion of authorized interpreters, asserting instead that meaning-making is a fundamentally distributed and democratic process.',
'Every act of reading is an act of creative interpretation, a collaboration between text and reader that produces something new each time.',
'Hierarchy in knowledge systems serves power, not understanding. It determines who gets to speak, who must listen, and what counts as legitimate knowledge.',
'The boundary between creator and consumer is an artificial construction, one that digital networks make increasingly untenable and obsolete.',
"This is the fundamental paradox of institutionalized knowledge. 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. What remains is a static representation, not the dynamic reality.",
"Understanding emerges not from rigid frameworks but from fluid engagement with ideas, people, and contexts. This fluidity is precisely what traditional systems attempt to eliminate.",
"Traditional institutions struggle with the natural promiscuity of ideas—the way concepts naturally migrate, mutate, and merge across boundaries that were meant to contain them.",
"Thinking without permission means refusing predetermined categories and challenging the gatekeepers who claim authority over legitimate thought.",
"The most valuable insights often come from unexpected juxtapositions, from bringing together ideas that were never meant to meet.",
"Anarchistic knowledge rejects the notion of authorized interpreters, asserting instead that meaning-making is a fundamentally distributed and democratic process.",
"Every act of reading is an act of creative interpretation, a collaboration between text and reader that produces something new each time.",
"Hierarchy in knowledge systems serves power, not understanding. It determines who gets to speak, who must listen, and what counts as legitimate knowledge.",
"The boundary between creator and consumer is an artificial construction, one that digital networks make increasingly untenable and obsolete.",
];
// Optional annotations (user comments on their highlights)
constannotations=[
'This perfectly captures the institutional problem',
'Key insight - worth revisiting',
'Reminds me of Deleuze on rhizomatic structures',
'Fundamental critique of academic gatekeeping',
'The core argument in one sentence',
"This perfectly captures the institutional problem",
"Key insight - worth revisiting",
"Reminds me of Deleuze on rhizomatic structures",
"Fundamental critique of academic gatekeeping",
"The core argument in one sentence",
null,// Some highlights have no annotation
'Important for understanding the broader thesis',
"Important for understanding the broader thesis",
null,
'Connects to earlier discussion on page 12',
"Connects to earlier discussion on page 12",
null,
];
// Mock pubkeys - MUST be exactly 64 hex characters
console.error("Failed to read test document:",error);
testContent=`= Deep Hierarchical Document Test
@ -65,20 +69,19 @@ A second main section to ensure we have balanced content at the top level.`;
@@ -65,20 +69,19 @@ A second main section to ensure we have balanced content at the top level.`;
}
describe("NKBIP-01 Publication Tree Processor",()=>{
test.test('Subsection content should be cleanly separated',()=>{
test.test("Subsection content should be cleanly separated",()=>{
// "=== Why Investigate the Nature of Knowledge?" subsection
constexpectedSubsectionContent=`Understanding the nature of knowledge itself is fundamental, distinct from simply studying how we learn or communicate. Knowledge exests first as representations within individuals, separate from how we interact with it...`;
constexpectedSubsectionContent=
`Understanding the nature of knowledge itself is fundamental, distinct from simply studying how we learn or communicate. Knowledge exests first as representations within individuals, separate from how we interact with it...`;
test.expect(expectedSubsectionContent).toContain("Understanding the nature");
});
test.test('Deep headers (====) should have proper newlines',()=>{
test.test("Deep headers (====) should have proper newlines",()=>{
// From "=== The Four Perspectives" section with ==== subsections
constexpectedFormatted=`
====1.TheBuildingBlocks(MaterialCause)
@ -230,188 +264,226 @@ Just as living organisms are made up of cells, knowledge systems are built from
@@ -230,188 +264,226 @@ Just as living organisms are made up of cells, knowledge systems are built from
@ -461,41 +533,46 @@ Traditionally, knowledge has been perceived as a static repository...
@@ -461,41 +533,46 @@ Traditionally, knowledge has been perceived as a static repository...
=====1.TheBuildingBlocks(MaterialCause)
Justaslivingorganismsaremadeupofcells...`,
metadata:{title:'Introduction: Knowledge as a Living Ecosystem'}
}
metadata:{title:"Introduction: Knowledge as a Living Ecosystem"},
},
// ... 4 more sections (Material Cause, Formal Cause, Efficient Cause, Final Cause)