{#each Object.entries(relayStatuses) as [relay, status]}
-
+
{/each}
- {#if !foundEvent && Object.values(relayStatuses).some(s => s === 'pending')}
-
Searching relays...
+ {#if !foundEvent && Object.values(relayStatuses).some((s) => s === "pending")}
+
+ Searching relays...
+
{/if}
-
\ No newline at end of file
+
diff --git a/src/lib/components/Login.svelte b/src/lib/components/Login.svelte
index e0d1171..e24490d 100644
--- a/src/lib/components/Login.svelte
+++ b/src/lib/components/Login.svelte
@@ -1,21 +1,27 @@
-
-{/if}
\ No newline at end of file
+{/if}
diff --git a/src/lib/components/RelayDisplay.svelte b/src/lib/components/RelayDisplay.svelte
index ffaa963..f717d42 100644
--- a/src/lib/components/RelayDisplay.svelte
+++ b/src/lib/components/RelayDisplay.svelte
@@ -1,14 +1,16 @@
-
- You are not signed in. Some relays require authentication and may not be accessible.
- Sign in to access all relays.
+ You are not signed in. Some relays require authentication and may not be
+ accessible. Sign in to access all relays.
diff --git a/src/lib/navigator/EventNetwork/types.ts b/src/lib/navigator/EventNetwork/types.ts
index db2d46b..1667a3a 100644
--- a/src/lib/navigator/EventNetwork/types.ts
+++ b/src/lib/navigator/EventNetwork/types.ts
@@ -1,6 +1,6 @@
/**
* Type definitions for the Event Network visualization
- *
+ *
* This module defines the core data structures used in the D3 force-directed
* graph visualization of Nostr events.
*/
@@ -12,13 +12,13 @@ import type { NDKEvent } from "@nostr-dev-kit/ndk";
* Represents the physical properties of a node in the simulation
*/
export interface SimulationNodeDatum {
- index?: number; // Node index in the simulation
- x?: number; // X position
- y?: number; // Y position
- vx?: number; // X velocity
- vy?: number; // Y velocity
- fx?: number | null; // Fixed X position (when node is pinned)
- fy?: number | null; // Fixed Y position (when node is pinned)
+ index?: number; // Node index in the simulation
+ x?: number; // X position
+ y?: number; // Y position
+ vx?: number; // X velocity
+ vy?: number; // Y velocity
+ fx?: number | null; // Fixed X position (when node is pinned)
+ fy?: number | null; // Fixed Y position (when node is pinned)
}
/**
@@ -26,9 +26,9 @@ export interface SimulationNodeDatum {
* Represents connections between nodes
*/
export interface SimulationLinkDatum {
- source: NodeType | string | number; // Source node or identifier
- target: NodeType | string | number; // Target node or identifier
- index?: number; // Link index in the simulation
+ source: NodeType | string | number; // Source node or identifier
+ target: NodeType | string | number; // Target node or identifier
+ index?: number; // Link index in the simulation
}
/**
@@ -36,17 +36,17 @@ export interface SimulationLinkDatum {
* Extends the base simulation node with Nostr event-specific properties
*/
export interface NetworkNode extends SimulationNodeDatum {
- id: string; // Unique identifier (event ID)
- event?: NDKEvent; // Reference to the original NDK event
- level: number; // Hierarchy level in the network
- kind: number; // Nostr event kind (30040 for index, 30041/30818 for content)
- title: string; // Event title
- content: string; // Event content
- author: string; // Author's public key
- type: "Index" | "Content"; // Node type classification
- naddr?: string; // NIP-19 naddr identifier
- nevent?: string; // NIP-19 nevent identifier
- isContainer?: boolean; // Whether this node is a container (index)
+ id: string; // Unique identifier (event ID)
+ event?: NDKEvent; // Reference to the original NDK event
+ level: number; // Hierarchy level in the network
+ kind: number; // Nostr event kind (30040 for index, 30041/30818 for content)
+ title: string; // Event title
+ content: string; // Event content
+ author: string; // Author's public key
+ type: "Index" | "Content"; // Node type classification
+ naddr?: string; // NIP-19 naddr identifier
+ nevent?: string; // NIP-19 nevent identifier
+ isContainer?: boolean; // Whether this node is a container (index)
}
/**
@@ -54,17 +54,17 @@ export interface NetworkNode extends SimulationNodeDatum {
* Extends the base simulation link with event-specific properties
*/
export interface NetworkLink extends SimulationLinkDatum {
- source: NetworkNode; // Source node (overridden to be more specific)
- target: NetworkNode; // Target node (overridden to be more specific)
- isSequential: boolean; // Whether this link represents a sequential relationship
+ source: NetworkNode; // Source node (overridden to be more specific)
+ target: NetworkNode; // Target node (overridden to be more specific)
+ isSequential: boolean; // Whether this link represents a sequential relationship
}
/**
* Represents the complete graph data for visualization
*/
export interface GraphData {
- nodes: NetworkNode[]; // All nodes in the graph
- links: NetworkLink[]; // All links in the graph
+ nodes: NetworkNode[]; // All nodes in the graph
+ links: NetworkLink[]; // All links in the graph
}
/**
@@ -72,8 +72,8 @@ export interface GraphData {
* Used to track relationships and build the final graph
*/
export interface GraphState {
- nodeMap: Map; // Maps event IDs to nodes
- links: NetworkLink[]; // All links in the graph
- eventMap: Map; // Maps event IDs to original events
- referencedIds: Set; // Set of event IDs referenced by other events
+ nodeMap: Map; // Maps event IDs to nodes
+ links: NetworkLink[]; // All links in the graph
+ eventMap: Map; // Maps event IDs to original events
+ referencedIds: Set; // Set of event IDs referenced by other events
}
diff --git a/src/lib/navigator/EventNetwork/utils/forceSimulation.ts b/src/lib/navigator/EventNetwork/utils/forceSimulation.ts
index 34731b3..dbcb1e0 100644
--- a/src/lib/navigator/EventNetwork/utils/forceSimulation.ts
+++ b/src/lib/navigator/EventNetwork/utils/forceSimulation.ts
@@ -1,6 +1,6 @@
/**
* D3 Force Simulation Utilities
- *
+ *
* This module provides utilities for creating and managing D3 force-directed
* graph simulations for the event network visualization.
*/
@@ -27,18 +27,18 @@ function debug(...args: any[]) {
* Provides type safety for simulation operations
*/
export interface Simulation {
- nodes(): NodeType[];
- nodes(nodes: NodeType[]): this;
- alpha(): number;
- alpha(alpha: number): this;
- alphaTarget(): number;
- alphaTarget(target: number): this;
- restart(): this;
- stop(): this;
- tick(): this;
- on(type: string, listener: (this: this) => void): this;
- force(name: string): any;
- force(name: string, force: any): this;
+ nodes(): NodeType[];
+ nodes(nodes: NodeType[]): this;
+ alpha(): number;
+ alpha(alpha: number): this;
+ alphaTarget(): number;
+ alphaTarget(target: number): this;
+ restart(): this;
+ stop(): this;
+ tick(): this;
+ on(type: string, listener: (this: this) => void): this;
+ force(name: string): any;
+ force(name: string, force: any): this;
}
/**
@@ -46,155 +46,173 @@ export interface Simulation {
* Provides type safety for drag operations
*/
export interface D3DragEvent {
- active: number;
- sourceEvent: any;
- subject: Subject;
- x: number;
- y: number;
- dx: number;
- dy: number;
- identifier: string | number;
+ active: number;
+ sourceEvent: any;
+ subject: Subject;
+ x: number;
+ y: number;
+ dx: number;
+ dy: number;
+ identifier: string | number;
}
/**
* Updates a node's velocity by applying a force
- *
+ *
* @param node - The node to update
* @param deltaVx - Change in x velocity
* @param deltaVy - Change in y velocity
*/
export function updateNodeVelocity(
- node: NetworkNode,
- deltaVx: number,
- deltaVy: number
+ node: NetworkNode,
+ deltaVx: number,
+ deltaVy: number,
) {
- debug("Updating node velocity", {
- nodeId: node.id,
- currentVx: node.vx,
- currentVy: node.vy,
- deltaVx,
- deltaVy
- });
-
- if (typeof node.vx === "number" && typeof node.vy === "number") {
- node.vx = node.vx - deltaVx;
- node.vy = node.vy - deltaVy;
- debug("New velocity", { nodeId: node.id, vx: node.vx, vy: node.vy });
- } else {
- debug("Node velocity not defined", { nodeId: node.id });
- }
+ debug("Updating node velocity", {
+ nodeId: node.id,
+ currentVx: node.vx,
+ currentVy: node.vy,
+ deltaVx,
+ deltaVy,
+ });
+
+ if (typeof node.vx === "number" && typeof node.vy === "number") {
+ node.vx = node.vx - deltaVx;
+ node.vy = node.vy - deltaVy;
+ debug("New velocity", { nodeId: node.id, vx: node.vx, vy: node.vy });
+ } else {
+ debug("Node velocity not defined", { nodeId: node.id });
+ }
}
/**
* Applies a logarithmic gravity force pulling the node toward the center
- *
+ *
* The logarithmic scale ensures that nodes far from the center experience
* stronger gravity, preventing them from drifting too far away.
- *
+ *
* @param node - The node to apply gravity to
* @param centerX - X coordinate of the center
* @param centerY - Y coordinate of the center
* @param alpha - Current simulation alpha (cooling factor)
*/
export function applyGlobalLogGravity(
- node: NetworkNode,
- centerX: number,
- centerY: number,
- alpha: number,
+ node: NetworkNode,
+ centerX: number,
+ centerY: number,
+ alpha: number,
) {
- const dx = (node.x ?? 0) - centerX;
- const dy = (node.y ?? 0) - centerY;
- const distance = Math.sqrt(dx * dx + dy * dy);
+ const dx = (node.x ?? 0) - centerX;
+ const dy = (node.y ?? 0) - centerY;
+ const distance = Math.sqrt(dx * dx + dy * dy);
- if (distance === 0) return;
+ if (distance === 0) return;
- const force = Math.log(distance + 1) * GRAVITY_STRENGTH * alpha;
- updateNodeVelocity(node, (dx / distance) * force, (dy / distance) * force);
+ const force = Math.log(distance + 1) * GRAVITY_STRENGTH * alpha;
+ updateNodeVelocity(node, (dx / distance) * force, (dy / distance) * force);
}
/**
* Applies gravity between connected nodes
- *
+ *
* This creates a cohesive force that pulls connected nodes toward their
* collective center of gravity, creating more meaningful clusters.
- *
+ *
* @param node - The node to apply connected gravity to
* @param links - All links in the network
* @param alpha - Current simulation alpha (cooling factor)
*/
export function applyConnectedGravity(
- node: NetworkNode,
- links: NetworkLink[],
- alpha: number,
+ node: NetworkNode,
+ links: NetworkLink[],
+ alpha: number,
) {
- // Find all nodes connected to this node
- const connectedNodes = links
- .filter(link => link.source.id === node.id || link.target.id === node.id)
- .map(link => link.source.id === node.id ? link.target : link.source);
+ // Find all nodes connected to this node
+ const connectedNodes = links
+ .filter((link) => link.source.id === node.id || link.target.id === node.id)
+ .map((link) => (link.source.id === node.id ? link.target : link.source));
- if (connectedNodes.length === 0) return;
+ if (connectedNodes.length === 0) return;
- // Calculate center of gravity of connected nodes
- const cogX = d3.mean(connectedNodes, (n: NetworkNode) => n.x);
- const cogY = d3.mean(connectedNodes, (n: NetworkNode) => n.y);
+ // Calculate center of gravity of connected nodes
+ const cogX = d3.mean(connectedNodes, (n: NetworkNode) => n.x);
+ const cogY = d3.mean(connectedNodes, (n: NetworkNode) => n.y);
- if (cogX === undefined || cogY === undefined) return;
+ if (cogX === undefined || cogY === undefined) return;
- // Calculate force direction and magnitude
- const dx = (node.x ?? 0) - cogX;
- const dy = (node.y ?? 0) - cogY;
- const distance = Math.sqrt(dx * dx + dy * dy);
+ // Calculate force direction and magnitude
+ const dx = (node.x ?? 0) - cogX;
+ const dy = (node.y ?? 0) - cogY;
+ const distance = Math.sqrt(dx * dx + dy * dy);
- if (distance === 0) return;
+ if (distance === 0) return;
- // Apply force proportional to distance
- const force = distance * CONNECTED_GRAVITY_STRENGTH * alpha;
- updateNodeVelocity(node, (dx / distance) * force, (dy / distance) * force);
+ // Apply force proportional to distance
+ const force = distance * CONNECTED_GRAVITY_STRENGTH * alpha;
+ updateNodeVelocity(node, (dx / distance) * force, (dy / distance) * force);
}
/**
* Sets up drag behavior for nodes
- *
+ *
* This enables interactive dragging of nodes in the visualization.
- *
+ *
* @param simulation - The D3 force simulation
* @param warmupClickEnergy - Alpha target when dragging starts (0-1)
* @returns D3 drag behavior configured for the simulation
*/
export function setupDragHandlers(
- simulation: Simulation,
- warmupClickEnergy: number = 0.9
+ simulation: Simulation,
+ warmupClickEnergy: number = 0.9,
) {
- return d3
- .drag()
- .on("start", (event: D3DragEvent, d: NetworkNode) => {
- // Warm up simulation if it's cooled down
- if (!event.active) {
- simulation.alphaTarget(warmupClickEnergy).restart();
- }
- // Fix node position at current location
- d.fx = d.x;
- d.fy = d.y;
- })
- .on("drag", (event: D3DragEvent, d: NetworkNode) => {
- // Update fixed position to mouse position
- d.fx = event.x;
- d.fy = event.y;
- })
- .on("end", (event: D3DragEvent, d: NetworkNode) => {
- // Cool down simulation when drag ends
- if (!event.active) {
- simulation.alphaTarget(0);
- }
- // Release fixed position
- d.fx = null;
- d.fy = null;
- });
+ return d3
+ .drag()
+ .on(
+ "start",
+ (
+ event: D3DragEvent,
+ d: NetworkNode,
+ ) => {
+ // Warm up simulation if it's cooled down
+ if (!event.active) {
+ simulation.alphaTarget(warmupClickEnergy).restart();
+ }
+ // Fix node position at current location
+ d.fx = d.x;
+ d.fy = d.y;
+ },
+ )
+ .on(
+ "drag",
+ (
+ event: D3DragEvent,
+ d: NetworkNode,
+ ) => {
+ // Update fixed position to mouse position
+ d.fx = event.x;
+ d.fy = event.y;
+ },
+ )
+ .on(
+ "end",
+ (
+ event: D3DragEvent,
+ d: NetworkNode,
+ ) => {
+ // Cool down simulation when drag ends
+ if (!event.active) {
+ simulation.alphaTarget(0);
+ }
+ // Release fixed position
+ d.fx = null;
+ d.fy = null;
+ },
+ );
}
/**
* Creates a D3 force simulation for the network
- *
+ *
* @param nodes - Array of network nodes
* @param links - Array of network links
* @param nodeRadius - Radius of node circles
@@ -202,34 +220,35 @@ export function setupDragHandlers(
* @returns Configured D3 force simulation
*/
export function createSimulation(
- nodes: NetworkNode[],
- links: NetworkLink[],
- nodeRadius: number,
- linkDistance: number
+ nodes: NetworkNode[],
+ links: NetworkLink[],
+ nodeRadius: number,
+ linkDistance: number,
): Simulation {
- debug("Creating simulation", {
- nodeCount: nodes.length,
- linkCount: links.length,
- nodeRadius,
- linkDistance
- });
-
- try {
- // Create the simulation with nodes
- const simulation = d3
- .forceSimulation(nodes)
- .force(
- "link",
- d3.forceLink(links)
- .id((d: NetworkNode) => d.id)
- .distance(linkDistance * 0.1)
- )
- .force("collide", d3.forceCollide().radius(nodeRadius * 4));
-
- debug("Simulation created successfully");
- return simulation;
- } catch (error) {
- console.error("Error creating simulation:", error);
- throw error;
- }
+ debug("Creating simulation", {
+ nodeCount: nodes.length,
+ linkCount: links.length,
+ nodeRadius,
+ linkDistance,
+ });
+
+ try {
+ // Create the simulation with nodes
+ const simulation = d3
+ .forceSimulation(nodes)
+ .force(
+ "link",
+ d3
+ .forceLink(links)
+ .id((d: NetworkNode) => d.id)
+ .distance(linkDistance * 0.1),
+ )
+ .force("collide", d3.forceCollide().radius(nodeRadius * 4));
+
+ debug("Simulation created successfully");
+ return simulation;
+ } catch (error) {
+ console.error("Error creating simulation:", error);
+ throw error;
+ }
}
diff --git a/src/lib/navigator/EventNetwork/utils/networkBuilder.ts b/src/lib/navigator/EventNetwork/utils/networkBuilder.ts
index 236f702..e05db37 100644
--- a/src/lib/navigator/EventNetwork/utils/networkBuilder.ts
+++ b/src/lib/navigator/EventNetwork/utils/networkBuilder.ts
@@ -1,6 +1,6 @@
/**
* Network Builder Utilities
- *
+ *
* This module provides utilities for building a network graph from Nostr events.
* It handles the creation of nodes and links, and the processing of event relationships.
*/
@@ -9,7 +9,7 @@ import type { NDKEvent } from "@nostr-dev-kit/ndk";
import type { NetworkNode, NetworkLink, GraphData, GraphState } from "../types";
import { nip19 } from "nostr-tools";
import { standardRelays } from "$lib/consts";
-import { getMatchingTags } from '$lib/utils/nostrUtils';
+import { getMatchingTags } from "$lib/utils/nostrUtils";
// Configuration
const DEBUG = false; // Set to true to enable debug logging
@@ -27,165 +27,169 @@ function debug(...args: any[]) {
/**
* Creates a NetworkNode from an NDKEvent
- *
+ *
* Extracts relevant information from the event and creates a node representation
* for the visualization.
- *
+ *
* @param event - The Nostr event to convert to a node
* @param level - The hierarchy level of the node (default: 0)
* @returns A NetworkNode object representing the event
*/
export function createNetworkNode(
- event: NDKEvent,
- level: number = 0
+ event: NDKEvent,
+ level: number = 0,
): NetworkNode {
- debug("Creating network node", { eventId: event.id, kind: event.kind, level });
-
- const isContainer = event.kind === INDEX_EVENT_KIND;
- const nodeType = isContainer ? "Index" : "Content";
+ debug("Creating network node", {
+ eventId: event.id,
+ kind: event.kind,
+ level,
+ });
- // Create the base node with essential properties
- const node: NetworkNode = {
+ const isContainer = event.kind === INDEX_EVENT_KIND;
+ const nodeType = isContainer ? "Index" : "Content";
+
+ // Create the base node with essential properties
+ const node: NetworkNode = {
+ id: event.id,
+ event,
+ isContainer,
+ level,
+ title: event.getMatchingTags("title")?.[0]?.[1] || "Untitled",
+ content: event.content || "",
+ author: event.pubkey || "",
+ kind: event.kind || CONTENT_EVENT_KIND, // Default to content event kind if undefined
+ type: nodeType,
+ };
+
+ // Add NIP-19 identifiers if possible
+ if (event.kind && event.pubkey) {
+ try {
+ const dTag = event.getMatchingTags("d")?.[0]?.[1] || "";
+
+ // Create naddr (NIP-19 address) for the event
+ node.naddr = nip19.naddrEncode({
+ pubkey: event.pubkey,
+ identifier: dTag,
+ kind: event.kind,
+ relays: standardRelays,
+ });
+
+ // Create nevent (NIP-19 event reference) for the event
+ node.nevent = nip19.neventEncode({
id: event.id,
- event,
- isContainer,
- level,
- title: event.getMatchingTags("title")?.[0]?.[1] || "Untitled",
- content: event.content || "",
- author: event.pubkey || "",
- kind: event.kind || CONTENT_EVENT_KIND, // Default to content event kind if undefined
- type: nodeType,
- };
-
- // Add NIP-19 identifiers if possible
- if (event.kind && event.pubkey) {
- try {
- const dTag = event.getMatchingTags("d")?.[0]?.[1] || "";
-
- // Create naddr (NIP-19 address) for the event
- node.naddr = nip19.naddrEncode({
- pubkey: event.pubkey,
- identifier: dTag,
- kind: event.kind,
- relays: standardRelays,
- });
-
- // Create nevent (NIP-19 event reference) for the event
- node.nevent = nip19.neventEncode({
- id: event.id,
- relays: standardRelays,
- kind: event.kind,
- });
- } catch (error) {
- console.warn("Failed to generate identifiers for node:", error);
- }
+ relays: standardRelays,
+ kind: event.kind,
+ });
+ } catch (error) {
+ console.warn("Failed to generate identifiers for node:", error);
}
+ }
- return node;
+ return node;
}
/**
* Creates a map of event IDs to events for quick lookup
- *
+ *
* @param events - Array of Nostr events
* @returns Map of event IDs to events
*/
export function createEventMap(events: NDKEvent[]): Map {
- debug("Creating event map", { eventCount: events.length });
-
- const eventMap = new Map();
- events.forEach((event) => {
- if (event.id) {
- eventMap.set(event.id, event);
- }
- });
-
- debug("Event map created", { mapSize: eventMap.size });
- return eventMap;
+ debug("Creating event map", { eventCount: events.length });
+
+ const eventMap = new Map();
+ events.forEach((event) => {
+ if (event.id) {
+ eventMap.set(event.id, event);
+ }
+ });
+
+ debug("Event map created", { mapSize: eventMap.size });
+ return eventMap;
}
/**
* Extracts an event ID from an 'a' tag
- *
+ *
* @param tag - The tag array from a Nostr event
* @returns The event ID or null if not found
*/
export function extractEventIdFromATag(tag: string[]): string | null {
- return tag[3] || null;
+ return tag[3] || null;
}
/**
* Generates a deterministic color for an event based on its ID
- *
+ *
* This creates visually distinct colors for different index events
* while ensuring the same event always gets the same color.
- *
+ *
* @param eventId - The event ID to generate a color for
* @returns An HSL color string
*/
export function getEventColor(eventId: string): string {
- // Use first 4 characters of event ID as a hex number
- const num = parseInt(eventId.slice(0, 4), 16);
- // Convert to a hue value (0-359)
- const hue = num % 360;
- // Use fixed saturation and lightness for pastel colors
- const saturation = 70;
- const lightness = 75;
- return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
+ // Use first 4 characters of event ID as a hex number
+ const num = parseInt(eventId.slice(0, 4), 16);
+ // Convert to a hue value (0-359)
+ const hue = num % 360;
+ // Use fixed saturation and lightness for pastel colors
+ const saturation = 70;
+ const lightness = 75;
+ return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
}
/**
* Initializes the graph state from a set of events
- *
+ *
* Creates nodes for all events and identifies referenced events.
- *
+ *
* @param events - Array of Nostr events
* @returns Initial graph state
*/
export function initializeGraphState(events: NDKEvent[]): GraphState {
- debug("Initializing graph state", { eventCount: events.length });
-
- const nodeMap = new Map();
- const eventMap = createEventMap(events);
-
- // Create initial nodes for all events
- events.forEach((event) => {
- if (!event.id) return;
- const node = createNetworkNode(event);
- nodeMap.set(event.id, node);
+ debug("Initializing graph state", { eventCount: events.length });
+
+ const nodeMap = new Map();
+ const eventMap = createEventMap(events);
+
+ // Create initial nodes for all events
+ events.forEach((event) => {
+ if (!event.id) return;
+ const node = createNetworkNode(event);
+ nodeMap.set(event.id, node);
+ });
+ debug("Node map created", { nodeCount: nodeMap.size });
+
+ // Build set of referenced event IDs to identify root events
+ const referencedIds = new Set();
+ events.forEach((event) => {
+ const aTags = getMatchingTags(event, "a");
+ debug("Processing a-tags for event", {
+ eventId: event.id,
+ aTagCount: aTags.length,
});
- debug("Node map created", { nodeCount: nodeMap.size });
-
- // Build set of referenced event IDs to identify root events
- const referencedIds = new Set();
- events.forEach((event) => {
- const aTags = getMatchingTags(event, "a");
- debug("Processing a-tags for event", {
- eventId: event.id,
- aTagCount: aTags.length
- });
-
- aTags.forEach((tag) => {
- const id = extractEventIdFromATag(tag);
- if (id) referencedIds.add(id);
- });
+
+ aTags.forEach((tag) => {
+ const id = extractEventIdFromATag(tag);
+ if (id) referencedIds.add(id);
});
- debug("Referenced IDs set created", { referencedCount: referencedIds.size });
-
- return {
- nodeMap,
- links: [],
- eventMap,
- referencedIds,
- };
+ });
+ debug("Referenced IDs set created", { referencedCount: referencedIds.size });
+
+ return {
+ nodeMap,
+ links: [],
+ eventMap,
+ referencedIds,
+ };
}
/**
* Processes a sequence of nodes referenced by an index event
- *
+ *
* Creates links between the index and its content, and between sequential content nodes.
* Also processes nested indices recursively up to the maximum level.
- *
+ *
* @param sequence - Array of nodes in the sequence
* @param indexEvent - The index event referencing the sequence
* @param level - Current hierarchy level
@@ -193,149 +197,147 @@ export function initializeGraphState(events: NDKEvent[]): GraphState {
* @param maxLevel - Maximum hierarchy level to process
*/
export function processSequence(
- sequence: NetworkNode[],
- indexEvent: NDKEvent,
- level: number,
- state: GraphState,
- maxLevel: number,
+ sequence: NetworkNode[],
+ indexEvent: NDKEvent,
+ level: number,
+ state: GraphState,
+ maxLevel: number,
): void {
- // Stop if we've reached max level or have no nodes
- if (level >= maxLevel || sequence.length === 0) return;
+ // Stop if we've reached max level or have no nodes
+ if (level >= maxLevel || sequence.length === 0) return;
- // Set levels for all nodes in the sequence
- sequence.forEach((node) => {
- node.level = level + 1;
+ // Set levels for all nodes in the sequence
+ sequence.forEach((node) => {
+ node.level = level + 1;
+ });
+
+ // Create link from index to first content node
+ const indexNode = state.nodeMap.get(indexEvent.id);
+ if (indexNode && sequence[0]) {
+ state.links.push({
+ source: indexNode,
+ target: sequence[0],
+ isSequential: true,
});
+ }
- // Create link from index to first content node
- const indexNode = state.nodeMap.get(indexEvent.id);
- if (indexNode && sequence[0]) {
- state.links.push({
- source: indexNode,
- target: sequence[0],
- isSequential: true,
- });
- }
+ // Create sequential links between content nodes
+ for (let i = 0; i < sequence.length - 1; i++) {
+ const currentNode = sequence[i];
+ const nextNode = sequence[i + 1];
- // Create sequential links between content nodes
- for (let i = 0; i < sequence.length - 1; i++) {
- const currentNode = sequence[i];
- const nextNode = sequence[i + 1];
-
- state.links.push({
- source: currentNode,
- target: nextNode,
- isSequential: true,
- });
-
- // Process nested indices recursively
- if (currentNode.isContainer) {
- processNestedIndex(currentNode, level + 1, state, maxLevel);
- }
- }
+ state.links.push({
+ source: currentNode,
+ target: nextNode,
+ isSequential: true,
+ });
- // Process the last node if it's an index
- const lastNode = sequence[sequence.length - 1];
- if (lastNode?.isContainer) {
- processNestedIndex(lastNode, level + 1, state, maxLevel);
+ // Process nested indices recursively
+ if (currentNode.isContainer) {
+ processNestedIndex(currentNode, level + 1, state, maxLevel);
}
+ }
+
+ // Process the last node if it's an index
+ const lastNode = sequence[sequence.length - 1];
+ if (lastNode?.isContainer) {
+ processNestedIndex(lastNode, level + 1, state, maxLevel);
+ }
}
/**
* Processes a nested index node
- *
+ *
* @param node - The index node to process
* @param level - Current hierarchy level
* @param state - Current graph state
* @param maxLevel - Maximum hierarchy level to process
*/
export function processNestedIndex(
- node: NetworkNode,
- level: number,
- state: GraphState,
- maxLevel: number,
+ node: NetworkNode,
+ level: number,
+ state: GraphState,
+ maxLevel: number,
): void {
- if (!node.isContainer || level >= maxLevel) return;
+ if (!node.isContainer || level >= maxLevel) return;
- const nestedEvent = state.eventMap.get(node.id);
- if (nestedEvent) {
- processIndexEvent(nestedEvent, level, state, maxLevel);
- }
+ const nestedEvent = state.eventMap.get(node.id);
+ if (nestedEvent) {
+ processIndexEvent(nestedEvent, level, state, maxLevel);
+ }
}
/**
* Processes an index event and its referenced content
- *
+ *
* @param indexEvent - The index event to process
* @param level - Current hierarchy level
* @param state - Current graph state
* @param maxLevel - Maximum hierarchy level to process
*/
export function processIndexEvent(
- indexEvent: NDKEvent,
- level: number,
- state: GraphState,
- maxLevel: number,
+ indexEvent: NDKEvent,
+ level: number,
+ state: GraphState,
+ maxLevel: number,
): void {
- if (level >= maxLevel) return;
+ if (level >= maxLevel) return;
- // Extract the sequence of nodes referenced by this index
- const sequence = getMatchingTags(indexEvent, "a")
- .map((tag) => extractEventIdFromATag(tag))
- .filter((id): id is string => id !== null)
- .map((id) => state.nodeMap.get(id))
- .filter((node): node is NetworkNode => node !== undefined);
+ // Extract the sequence of nodes referenced by this index
+ const sequence = getMatchingTags(indexEvent, "a")
+ .map((tag) => extractEventIdFromATag(tag))
+ .filter((id): id is string => id !== null)
+ .map((id) => state.nodeMap.get(id))
+ .filter((node): node is NetworkNode => node !== undefined);
- processSequence(sequence, indexEvent, level, state, maxLevel);
+ processSequence(sequence, indexEvent, level, state, maxLevel);
}
/**
* Generates a complete graph from a set of events
- *
+ *
* This is the main entry point for building the network visualization.
- *
+ *
* @param events - Array of Nostr events
* @param maxLevel - Maximum hierarchy level to process
* @returns Complete graph data for visualization
*/
-export function generateGraph(
- events: NDKEvent[],
- maxLevel: number
-): GraphData {
- debug("Generating graph", { eventCount: events.length, maxLevel });
-
- // Initialize the graph state
- const state = initializeGraphState(events);
-
- // Find root index events (those not referenced by other events)
- const rootIndices = events.filter(
- (e) => e.kind === INDEX_EVENT_KIND && e.id && !state.referencedIds.has(e.id)
- );
-
- debug("Found root indices", {
- rootCount: rootIndices.length,
- rootIds: rootIndices.map(e => e.id)
- });
-
- // Process each root index
- rootIndices.forEach((rootIndex) => {
- debug("Processing root index", {
- rootId: rootIndex.id,
- aTags: getMatchingTags(rootIndex, "a").length
- });
- processIndexEvent(rootIndex, 0, state, maxLevel);
- });
+export function generateGraph(events: NDKEvent[], maxLevel: number): GraphData {
+ debug("Generating graph", { eventCount: events.length, maxLevel });
+
+ // Initialize the graph state
+ const state = initializeGraphState(events);
+
+ // Find root index events (those not referenced by other events)
+ const rootIndices = events.filter(
+ (e) =>
+ e.kind === INDEX_EVENT_KIND && e.id && !state.referencedIds.has(e.id),
+ );
- // Create the final graph data
- const result = {
- nodes: Array.from(state.nodeMap.values()),
- links: state.links,
- };
-
- debug("Graph generation complete", {
- nodeCount: result.nodes.length,
- linkCount: result.links.length
+ debug("Found root indices", {
+ rootCount: rootIndices.length,
+ rootIds: rootIndices.map((e) => e.id),
+ });
+
+ // Process each root index
+ rootIndices.forEach((rootIndex) => {
+ debug("Processing root index", {
+ rootId: rootIndex.id,
+ aTags: getMatchingTags(rootIndex, "a").length,
});
-
- return result;
+ processIndexEvent(rootIndex, 0, state, maxLevel);
+ });
+
+ // Create the final graph data
+ const result = {
+ nodes: Array.from(state.nodeMap.values()),
+ links: state.links,
+ };
+
+ debug("Graph generation complete", {
+ nodeCount: result.nodes.length,
+ linkCount: result.links.length,
+ });
+
+ return result;
}
diff --git a/src/lib/ndk.ts b/src/lib/ndk.ts
index a518ff4..cbdf546 100644
--- a/src/lib/ndk.ts
+++ b/src/lib/ndk.ts
@@ -1,7 +1,20 @@
-import NDK, { NDKNip07Signer, NDKRelay, NDKRelayAuthPolicies, NDKRelaySet, NDKUser, NDKEvent } from '@nostr-dev-kit/ndk';
-import { get, writable, type Writable } from 'svelte/store';
-import { fallbackRelays, FeedType, loginStorageKey, standardRelays, anonymousRelays } from './consts';
-import { feedType } from './stores';
+import NDK, {
+ NDKNip07Signer,
+ NDKRelay,
+ NDKRelayAuthPolicies,
+ NDKRelaySet,
+ NDKUser,
+ NDKEvent,
+} from "@nostr-dev-kit/ndk";
+import { get, writable, type Writable } from "svelte/store";
+import {
+ fallbackRelays,
+ FeedType,
+ loginStorageKey,
+ standardRelays,
+ anonymousRelays,
+} from "./consts";
+import { feedType } from "./stores";
export const ndkInstance: Writable = writable();
@@ -31,50 +44,63 @@ class CustomRelayAuthPolicy {
*/
async authenticate(relay: NDKRelay): Promise {
if (!this.ndk.signer || !this.ndk.activeUser) {
- console.warn('[NDK.ts] No signer or active user available for relay authentication');
+ console.warn(
+ "[NDK.ts] No signer or active user available for relay authentication",
+ );
return;
}
try {
console.debug(`[NDK.ts] Setting up authentication for ${relay.url}`);
-
+
// Listen for AUTH challenges
- relay.on('auth', (challenge: string) => {
- console.debug(`[NDK.ts] Received AUTH challenge from ${relay.url}:`, challenge);
+ relay.on("auth", (challenge: string) => {
+ console.debug(
+ `[NDK.ts] Received AUTH challenge from ${relay.url}:`,
+ challenge,
+ );
this.challenges.set(relay.url, challenge);
this.handleAuthChallenge(relay, challenge);
});
// Listen for auth-required errors (handle via notice events)
- relay.on('notice', (message: string) => {
- if (message.includes('auth-required')) {
+ relay.on("notice", (message: string) => {
+ if (message.includes("auth-required")) {
console.debug(`[NDK.ts] Auth required from ${relay.url}:`, message);
this.handleAuthRequired(relay, message);
}
});
// Listen for successful authentication
- relay.on('authed', () => {
+ relay.on("authed", () => {
console.debug(`[NDK.ts] Successfully authenticated to ${relay.url}`);
});
// Listen for authentication failures
- relay.on('auth:failed', (error: any) => {
- console.error(`[NDK.ts] Authentication failed for ${relay.url}:`, error);
+ relay.on("auth:failed", (error: any) => {
+ console.error(
+ `[NDK.ts] Authentication failed for ${relay.url}:`,
+ error,
+ );
});
-
} catch (error) {
- console.error(`[NDK.ts] Error setting up authentication for ${relay.url}:`, error);
+ console.error(
+ `[NDK.ts] Error setting up authentication for ${relay.url}:`,
+ error,
+ );
}
}
/**
* Handles AUTH challenge from relay
*/
- private async handleAuthChallenge(relay: NDKRelay, challenge: string): Promise {
+ private async handleAuthChallenge(
+ relay: NDKRelay,
+ challenge: string,
+ ): Promise {
try {
if (!this.ndk.signer || !this.ndk.activeUser) {
- console.warn('[NDK.ts] No signer available for AUTH challenge');
+ console.warn("[NDK.ts] No signer available for AUTH challenge");
return;
}
@@ -83,35 +109,42 @@ class CustomRelayAuthPolicy {
kind: 22242,
created_at: Math.floor(Date.now() / 1000),
tags: [
- ['relay', relay.url],
- ['challenge', challenge]
+ ["relay", relay.url],
+ ["challenge", challenge],
],
- content: '',
- pubkey: this.ndk.activeUser.pubkey
+ content: "",
+ pubkey: this.ndk.activeUser.pubkey,
};
// Create and sign the authentication event using NDKEvent
const authNDKEvent = new NDKEvent(this.ndk, authEvent);
await authNDKEvent.sign();
-
+
// Send AUTH message to relay using the relay's publish method
await relay.publish(authNDKEvent);
console.debug(`[NDK.ts] Sent AUTH to ${relay.url}`);
-
} catch (error) {
- console.error(`[NDK.ts] Error handling AUTH challenge for ${relay.url}:`, error);
+ console.error(
+ `[NDK.ts] Error handling AUTH challenge for ${relay.url}:`,
+ error,
+ );
}
}
/**
* Handles auth-required error from relay
*/
- private async handleAuthRequired(relay: NDKRelay, message: string): Promise {
+ private async handleAuthRequired(
+ relay: NDKRelay,
+ message: string,
+ ): Promise {
const challenge = this.challenges.get(relay.url);
if (challenge) {
await this.handleAuthChallenge(relay, challenge);
} else {
- console.warn(`[NDK.ts] Auth required from ${relay.url} but no challenge available`);
+ console.warn(
+ `[NDK.ts] Auth required from ${relay.url} but no challenge available`,
+ );
}
}
}
@@ -120,26 +153,33 @@ class CustomRelayAuthPolicy {
* Checks if the current environment might cause WebSocket protocol downgrade
*/
export function checkEnvironmentForWebSocketDowngrade(): void {
- console.debug('[NDK.ts] Environment Check for WebSocket Protocol:');
-
- const isLocalhost = window.location.hostname === 'localhost' ||
- window.location.hostname === '127.0.0.1';
- const isHttp = window.location.protocol === 'http:';
- const isHttps = window.location.protocol === 'https:';
-
- console.debug('[NDK.ts] - Is localhost:', isLocalhost);
- console.debug('[NDK.ts] - Protocol:', window.location.protocol);
- console.debug('[NDK.ts] - Is HTTP:', isHttp);
- console.debug('[NDK.ts] - Is HTTPS:', isHttps);
-
+ console.debug("[NDK.ts] Environment Check for WebSocket Protocol:");
+
+ const isLocalhost =
+ window.location.hostname === "localhost" ||
+ window.location.hostname === "127.0.0.1";
+ const isHttp = window.location.protocol === "http:";
+ const isHttps = window.location.protocol === "https:";
+
+ console.debug("[NDK.ts] - Is localhost:", isLocalhost);
+ console.debug("[NDK.ts] - Protocol:", window.location.protocol);
+ console.debug("[NDK.ts] - Is HTTP:", isHttp);
+ console.debug("[NDK.ts] - Is HTTPS:", isHttps);
+
if (isLocalhost && isHttp) {
- console.warn('[NDK.ts] ⚠️ Running on localhost with HTTP - WebSocket downgrade to ws:// is expected');
- console.warn('[NDK.ts] This is normal for development environments');
+ console.warn(
+ "[NDK.ts] ⚠️ Running on localhost with HTTP - WebSocket downgrade to ws:// is expected",
+ );
+ console.warn("[NDK.ts] This is normal for development environments");
} else if (isHttp) {
- console.error('[NDK.ts] ❌ Running on HTTP - WebSocket connections will be insecure');
- console.error('[NDK.ts] Consider using HTTPS in production');
+ console.error(
+ "[NDK.ts] ❌ Running on HTTP - WebSocket connections will be insecure",
+ );
+ console.error("[NDK.ts] Consider using HTTPS in production");
} else if (isHttps) {
- console.debug('[NDK.ts] ✓ Running on HTTPS - Secure WebSocket connections should work');
+ console.debug(
+ "[NDK.ts] ✓ Running on HTTPS - Secure WebSocket connections should work",
+ );
}
}
@@ -147,24 +187,24 @@ export function checkEnvironmentForWebSocketDowngrade(): void {
* Checks WebSocket protocol support and logs diagnostic information
*/
export function checkWebSocketSupport(): void {
- console.debug('[NDK.ts] WebSocket Support Diagnostics:');
- console.debug('[NDK.ts] - Protocol:', window.location.protocol);
- console.debug('[NDK.ts] - Hostname:', window.location.hostname);
- console.debug('[NDK.ts] - Port:', window.location.port);
- console.debug('[NDK.ts] - User Agent:', navigator.userAgent);
-
+ console.debug("[NDK.ts] WebSocket Support Diagnostics:");
+ console.debug("[NDK.ts] - Protocol:", window.location.protocol);
+ console.debug("[NDK.ts] - Hostname:", window.location.hostname);
+ console.debug("[NDK.ts] - Port:", window.location.port);
+ console.debug("[NDK.ts] - User Agent:", navigator.userAgent);
+
// Test if secure WebSocket is supported
try {
- const testWs = new WebSocket('wss://echo.websocket.org');
+ const testWs = new WebSocket("wss://echo.websocket.org");
testWs.onopen = () => {
- console.debug('[NDK.ts] ✓ Secure WebSocket (wss://) is supported');
+ console.debug("[NDK.ts] ✓ Secure WebSocket (wss://) is supported");
testWs.close();
};
testWs.onerror = () => {
- console.warn('[NDK.ts] ✗ Secure WebSocket (wss://) may not be supported');
+ console.warn("[NDK.ts] ✗ Secure WebSocket (wss://) may not be supported");
};
} catch (error) {
- console.warn('[NDK.ts] ✗ WebSocket test failed:', error);
+ console.warn("[NDK.ts] ✗ WebSocket test failed:", error);
}
}
@@ -174,7 +214,10 @@ export function checkWebSocketSupport(): void {
* @param ndk The NDK instance
* @returns Promise that resolves to connection status
*/
-export async function testRelayConnection(relayUrl: string, ndk: NDK): Promise<{
+export async function testRelayConnection(
+ relayUrl: string,
+ ndk: NDK,
+): Promise<{
connected: boolean;
requiresAuth: boolean;
error?: string;
@@ -182,10 +225,10 @@ export async function testRelayConnection(relayUrl: string, ndk: NDK): Promise<{
}> {
return new Promise((resolve) => {
console.debug(`[NDK.ts] Testing connection to: ${relayUrl}`);
-
+
// Ensure the URL is using wss:// protocol
const secureUrl = ensureSecureWebSocket(relayUrl);
-
+
const relay = new NDKRelay(secureUrl, undefined, new NDK());
let authRequired = false;
let connected = false;
@@ -197,12 +240,12 @@ export async function testRelayConnection(relayUrl: string, ndk: NDK): Promise<{
resolve({
connected: false,
requiresAuth: authRequired,
- error: 'Connection timeout',
- actualUrl
+ error: "Connection timeout",
+ actualUrl,
});
}, 5000);
- relay.on('connect', () => {
+ relay.on("connect", () => {
console.debug(`[NDK.ts] Connected to ${secureUrl}`);
connected = true;
actualUrl = secureUrl;
@@ -212,27 +255,27 @@ export async function testRelayConnection(relayUrl: string, ndk: NDK): Promise<{
connected: true,
requiresAuth: authRequired,
error,
- actualUrl
+ actualUrl,
});
});
- relay.on('notice', (message: string) => {
- if (message.includes('auth-required')) {
+ relay.on("notice", (message: string) => {
+ if (message.includes("auth-required")) {
authRequired = true;
console.debug(`[NDK.ts] ${secureUrl} requires authentication`);
}
});
- relay.on('disconnect', () => {
+ relay.on("disconnect", () => {
if (!connected) {
- error = 'Connection failed';
+ error = "Connection failed";
console.error(`[NDK.ts] Failed to connect to ${secureUrl}`);
clearTimeout(timeout);
resolve({
connected: false,
requiresAuth: authRequired,
error,
- actualUrl
+ actualUrl,
});
}
});
@@ -278,7 +321,7 @@ export function clearLogin(): void {
* @param type The type of relay list to designate.
* @returns The constructed key.
*/
-function getRelayStorageKey(user: NDKUser, type: 'inbox' | 'outbox'): string {
+function getRelayStorageKey(user: NDKUser, type: "inbox" | "outbox"): string {
return `${loginStorageKey}/${user.pubkey}/${type}`;
}
@@ -288,14 +331,18 @@ function getRelayStorageKey(user: NDKUser, type: 'inbox' | 'outbox'): string {
* @param inboxes The user's inbox relays.
* @param outboxes The user's outbox relays.
*/
-function persistRelays(user: NDKUser, inboxes: Set, outboxes: Set): void {
+function persistRelays(
+ user: NDKUser,
+ inboxes: Set,
+ outboxes: Set,
+): void {
localStorage.setItem(
- getRelayStorageKey(user, 'inbox'),
- JSON.stringify(Array.from(inboxes).map(relay => relay.url))
+ getRelayStorageKey(user, "inbox"),
+ JSON.stringify(Array.from(inboxes).map((relay) => relay.url)),
);
localStorage.setItem(
- getRelayStorageKey(user, 'outbox'),
- JSON.stringify(Array.from(outboxes).map(relay => relay.url))
+ getRelayStorageKey(user, "outbox"),
+ JSON.stringify(Array.from(outboxes).map((relay) => relay.url)),
);
}
@@ -307,18 +354,20 @@ function persistRelays(user: NDKUser, inboxes: Set, outboxes: Set, Set] {
const inboxes = new Set(
- JSON.parse(localStorage.getItem(getRelayStorageKey(user, 'inbox')) ?? '[]')
+ JSON.parse(localStorage.getItem(getRelayStorageKey(user, "inbox")) ?? "[]"),
);
const outboxes = new Set(
- JSON.parse(localStorage.getItem(getRelayStorageKey(user, 'outbox')) ?? '[]')
+ JSON.parse(
+ localStorage.getItem(getRelayStorageKey(user, "outbox")) ?? "[]",
+ ),
);
return [inboxes, outboxes];
}
export function clearPersistedRelays(user: NDKUser): void {
- localStorage.removeItem(getRelayStorageKey(user, 'inbox'));
- localStorage.removeItem(getRelayStorageKey(user, 'outbox'));
+ localStorage.removeItem(getRelayStorageKey(user, "inbox"));
+ localStorage.removeItem(getRelayStorageKey(user, "outbox"));
}
/**
@@ -328,12 +377,14 @@ export function clearPersistedRelays(user: NDKUser): void {
*/
function ensureSecureWebSocket(url: string): string {
// Replace ws:// with wss:// if present
- const secureUrl = url.replace(/^ws:\/\//, 'wss://');
-
+ const secureUrl = url.replace(/^ws:\/\//, "wss://");
+
if (secureUrl !== url) {
- console.warn(`[NDK.ts] Protocol downgrade detected: ${url} -> ${secureUrl}`);
+ console.warn(
+ `[NDK.ts] Protocol downgrade detected: ${url} -> ${secureUrl}`,
+ );
}
-
+
return secureUrl;
}
@@ -342,21 +393,25 @@ function ensureSecureWebSocket(url: string): string {
*/
function createRelayWithAuth(url: string, ndk: NDK): NDKRelay {
console.debug(`[NDK.ts] Creating relay with URL: ${url}`);
-
+
// Ensure the URL is using wss:// protocol
const secureUrl = ensureSecureWebSocket(url);
-
- const relay = new NDKRelay(secureUrl, NDKRelayAuthPolicies.signIn({ ndk }), ndk);
-
+
+ const relay = new NDKRelay(
+ secureUrl,
+ NDKRelayAuthPolicies.signIn({ ndk }),
+ ndk,
+ );
+
// Set up custom authentication handling only if user is signed in
if (ndk.signer && ndk.activeUser) {
const authPolicy = new CustomRelayAuthPolicy(ndk);
- relay.on('connect', () => {
+ relay.on("connect", () => {
console.debug(`[NDK.ts] Relay connected: ${secureUrl}`);
authPolicy.authenticate(relay);
});
}
-
+
return relay;
}
@@ -364,15 +419,17 @@ export function getActiveRelays(ndk: NDK): NDKRelaySet {
// Use anonymous relays if user is not signed in
const isSignedIn = ndk.signer && ndk.activeUser;
const relays = isSignedIn ? standardRelays : anonymousRelays;
-
+
return get(feedType) === FeedType.UserRelays
? new NDKRelaySet(
- new Set(get(inboxRelays).map(relay => createRelayWithAuth(relay, ndk))),
- ndk
+ new Set(
+ get(inboxRelays).map((relay) => createRelayWithAuth(relay, ndk)),
+ ),
+ ndk,
)
: new NDKRelaySet(
- new Set(relays.map(relay => createRelayWithAuth(relay, ndk))),
- ndk
+ new Set(relays.map((relay) => createRelayWithAuth(relay, ndk))),
+ ndk,
);
}
@@ -383,17 +440,20 @@ export function getActiveRelays(ndk: NDK): NDKRelaySet {
*/
export function initNdk(): NDK {
const startingPubkey = getPersistedLogin();
- const [startingInboxes, _] = startingPubkey != null
- ? getPersistedRelays(new NDKUser({ pubkey: startingPubkey }))
- : [null, null];
+ const [startingInboxes, _] =
+ startingPubkey != null
+ ? getPersistedRelays(new NDKUser({ pubkey: startingPubkey }))
+ : [null, null];
// Ensure all relay URLs use secure WebSocket protocol
- const secureRelayUrls = (startingInboxes != null
- ? Array.from(startingInboxes.values())
- : anonymousRelays).map(ensureSecureWebSocket);
-
- console.debug('[NDK.ts] Initializing NDK with relay URLs:', secureRelayUrls);
-
+ const secureRelayUrls = (
+ startingInboxes != null
+ ? Array.from(startingInboxes.values())
+ : anonymousRelays
+ ).map(ensureSecureWebSocket);
+
+ console.debug("[NDK.ts] Initializing NDK with relay URLs:", secureRelayUrls);
+
const ndk = new NDK({
autoConnectUserRelays: true,
enableOutboxModel: true,
@@ -413,7 +473,9 @@ export function initNdk(): NDK {
* @throws If sign-in fails. This may because there is no accessible NIP-07 extension, or because
* NDK is unable to fetch the user's profile or relay lists.
*/
-export async function loginWithExtension(pubkey?: string): Promise {
+export async function loginWithExtension(
+ pubkey?: string,
+): Promise {
try {
const ndk = get(ndkInstance);
const signer = new NDKNip07Signer();
@@ -421,12 +483,13 @@ export async function loginWithExtension(pubkey?: string): Promise relay.url));
- outboxRelays.set(Array.from(outboxes ?? persistedOutboxes).map(relay => relay.url));
+ inboxRelays.set(
+ Array.from(inboxes ?? persistedInboxes).map((relay) => relay.url),
+ );
+ outboxRelays.set(
+ Array.from(outboxes ?? persistedOutboxes).map((relay) => relay.url),
+ );
persistRelays(signerUser, inboxes, outboxes);
@@ -471,14 +538,14 @@ export function logout(user: NDKUser): void {
async function getUserPreferredRelays(
ndk: NDK,
user: NDKUser,
- fallbacks: readonly string[] = fallbackRelays
+ fallbacks: readonly string[] = fallbackRelays,
): Promise<[Set, Set]> {
const relayList = await ndk.fetchEvent(
{
kinds: [10002],
authors: [user.pubkey],
},
- {
+ {
groupable: false,
skipVerification: false,
skipValidation: false,
@@ -497,12 +564,12 @@ async function getUserPreferredRelays(
if (relayType.write) outboxRelays.add(relay);
});
} else {
- relayList.tags.forEach(tag => {
+ relayList.tags.forEach((tag) => {
switch (tag[0]) {
- case 'r':
+ case "r":
inboxRelays.add(createRelayWithAuth(tag[1], ndk));
break;
- case 'w':
+ case "w":
outboxRelays.add(createRelayWithAuth(tag[1], ndk));
break;
default:
diff --git a/src/lib/parser.ts b/src/lib/parser.ts
index 5ec4cdd..b35c86f 100644
--- a/src/lib/parser.ts
+++ b/src/lib/parser.ts
@@ -1,5 +1,5 @@
-import NDK, { NDKEvent } from '@nostr-dev-kit/ndk';
-import asciidoctor from 'asciidoctor';
+import NDK, { NDKEvent } from "@nostr-dev-kit/ndk";
+import asciidoctor from "asciidoctor";
import type {
AbstractBlock,
AbstractNode,
@@ -9,11 +9,11 @@ import type {
Extensions,
Section,
ProcessorOptions,
-} from 'asciidoctor';
-import he from 'he';
-import { writable, type Writable } from 'svelte/store';
-import { zettelKinds } from './consts.ts';
-import { getMatchingTags } from '$lib/utils/nostrUtils';
+} from "asciidoctor";
+import he from "he";
+import { writable, type Writable } from "svelte/store";
+import { zettelKinds } from "./consts.ts";
+import { getMatchingTags } from "$lib/utils/nostrUtils";
interface IndexMetadata {
authors?: string[];
@@ -28,16 +28,16 @@ interface IndexMetadata {
export enum SiblingSearchDirection {
Previous,
- Next
+ Next,
}
export enum InsertLocation {
Before,
- After
+ After,
}
/**
- * @classdesc Pharos is an extension of the Asciidoctor class that adds Nostr Knowledge Base (NKB)
+ * @classdesc Pharos is an extension of the Asciidoctor class that adds Nostr Knowledge Base (NKB)
* features to core Asciidoctor functionality. Asciidoctor is used to parse an AsciiDoc document
* into an Abstract Syntax Tree (AST), and Phraos generates NKB events from the nodes in that tree.
* @class
@@ -46,12 +46,12 @@ export enum InsertLocation {
export default class Pharos {
/**
* Key to terminology used in the class:
- *
+ *
* Nostr Knowledge Base (NKB) entities:
* - Zettel: Bite-sized pieces of text contained within kind 30041 events.
* - Index: A kind 30040 event describing a collection of zettels or other Nostr events.
* - Event: The generic term for a Nostr event.
- *
+ *
* Asciidoctor entities:
* - Document: The entirety of an AsciiDoc document. The document title is denoted by a level 0
* header, and the document may contain metadata, such as author and edition, immediately below
@@ -112,7 +112,10 @@ export default class Pharos {
/**
* A map of index IDs to the IDs of the nodes they reference.
*/
- private indexToChildEventsMap: Map> = new Map>();
+ private indexToChildEventsMap: Map> = new Map<
+ string,
+ Set
+ >();
/**
* A map of node IDs to the Nostr event IDs of the events they generate.
@@ -160,34 +163,37 @@ export default class Pharos {
*/
private async loadAdvancedExtensions(): Promise {
try {
- const { createAdvancedExtensions } = await import('./utils/markup/asciidoctorExtensions');
+ const { createAdvancedExtensions } = await import(
+ "./utils/markup/asciidoctorExtensions"
+ );
const advancedExtensions = createAdvancedExtensions();
// Note: Extensions merging might not be available in this version
// We'll handle this in the parse method instead
} catch (error) {
- console.warn('Advanced extensions not available:', error);
+ console.warn("Advanced extensions not available:", error);
}
}
parse(content: string, options?: ProcessorOptions | undefined): void {
-
// Ensure the content is valid AsciiDoc and has a header and the doctype book
content = ensureAsciiDocHeader(content);
-
+
try {
const mergedAttributes = Object.assign(
{},
- options && typeof options.attributes === 'object' ? options.attributes : {},
- { 'source-highlighter': 'highlightjs' }
+ options && typeof options.attributes === "object"
+ ? options.attributes
+ : {},
+ { "source-highlighter": "highlightjs" },
);
this.html = this.asciidoctor.convert(content, {
...options,
- 'extension_registry': this.pharosExtensions,
+ extension_registry: this.pharosExtensions,
attributes: mergedAttributes,
}) as string | Document | undefined;
} catch (error) {
console.error(error);
- throw new Error('Failed to parse AsciiDoc document.');
+ throw new Error("Failed to parse AsciiDoc document.");
}
}
@@ -199,10 +205,10 @@ export default class Pharos {
async fetch(event: NDKEvent | string): Promise {
let content: string;
- if (typeof event === 'string') {
+ if (typeof event === "string") {
const index = await this.ndk.fetchEvent({ ids: [event] });
if (!index) {
- throw new Error('Failed to fetch publication.');
+ throw new Error("Failed to fetch publication.");
}
content = await this.getPublicationContent(index);
@@ -224,7 +230,7 @@ export default class Pharos {
/**
* Generates and stores Nostr events from the parsed AsciiDoc document. The events can be
* modified via the parser's API and retrieved via the `getEvents()` method.
- * @param pubkey The public key (as a hex string) of the user that will sign and publish the
+ * @param pubkey The public key (as a hex string) of the user that will sign and publish the
* events.
*/
generate(pubkey: string): void {
@@ -252,7 +258,7 @@ export default class Pharos {
* @returns The HTML content of the converted document.
*/
getHtml(): string {
- return this.html?.toString() || '';
+ return this.html?.toString() || "";
}
/**
@@ -260,7 +266,7 @@ export default class Pharos {
* @remarks The root index ID may be used to retrieve metadata or children from the root index.
*/
getRootIndexId(): string {
- return this.normalizeId(this.rootNodeId) ?? '';
+ return this.normalizeId(this.rootNodeId) ?? "";
}
/**
@@ -268,7 +274,7 @@ export default class Pharos {
*/
getIndexTitle(id: string): string | undefined {
const section = this.nodes.get(id) as Section;
- const title = section.getTitle() ?? '';
+ const title = section.getTitle() ?? "";
return he.decode(title);
}
@@ -276,16 +282,18 @@ export default class Pharos {
* @returns The IDs of any child indices of the index with the given ID.
*/
getChildIndexIds(id: string): string[] {
- return Array.from(this.indexToChildEventsMap.get(id) ?? [])
- .filter(id => this.eventToKindMap.get(id) === 30040);
+ return Array.from(this.indexToChildEventsMap.get(id) ?? []).filter(
+ (id) => this.eventToKindMap.get(id) === 30040,
+ );
}
/**
* @returns The IDs of any child zettels of the index with the given ID.
*/
getChildZettelIds(id: string): string[] {
- return Array.from(this.indexToChildEventsMap.get(id) ?? [])
- .filter(id => this.eventToKindMap.get(id) !== 30040);
+ return Array.from(this.indexToChildEventsMap.get(id) ?? []).filter(
+ (id) => this.eventToKindMap.get(id) !== 30040,
+ );
}
/**
@@ -307,8 +315,8 @@ export default class Pharos {
const block = this.nodes.get(normalizedId!) as AbstractBlock;
switch (block.getContext()) {
- case 'paragraph':
- return block.getContent() ?? '';
+ case "paragraph":
+ return block.getContent() ?? "";
}
return block.convert();
@@ -324,9 +332,9 @@ export default class Pharos {
if (!normalizedId || !this.nodes.has(normalizedId)) {
return false;
}
-
+
const context = this.eventToContextMap.get(normalizedId);
- return context === 'floating_title';
+ return context === "floating_title";
}
/**
@@ -361,7 +369,7 @@ export default class Pharos {
getNearestSibling(
targetDTag: string,
depth: number,
- direction: SiblingSearchDirection
+ direction: SiblingSearchDirection,
): [string | null, string | null] {
const eventsAtLevel = this.eventsByLevelMap.get(depth);
if (!eventsAtLevel) {
@@ -371,13 +379,17 @@ export default class Pharos {
const targetIndex = eventsAtLevel.indexOf(targetDTag);
if (targetIndex === -1) {
- throw new Error(`The event indicated by #d:${targetDTag} does not exist at level ${depth} of the event tree.`);
+ throw new Error(
+ `The event indicated by #d:${targetDTag} does not exist at level ${depth} of the event tree.`,
+ );
}
const parentDTag = this.getParent(targetDTag);
if (!parentDTag) {
- throw new Error(`The event indicated by #d:${targetDTag} does not have a parent.`);
+ throw new Error(
+ `The event indicated by #d:${targetDTag} does not have a parent.`,
+ );
}
const grandparentDTag = this.getParent(parentDTag);
@@ -395,7 +407,10 @@ export default class Pharos {
// If the target is the last node at its level and we're searching for a next sibling,
// look among the siblings of the target's parent at the previous level.
- if (targetIndex === eventsAtLevel.length - 1 && direction === SiblingSearchDirection.Next) {
+ if (
+ targetIndex === eventsAtLevel.length - 1 &&
+ direction === SiblingSearchDirection.Next
+ ) {
// * Base case: The target is at the last level of the tree and has no subsequent sibling.
if (!grandparentDTag) {
return [null, null];
@@ -406,10 +421,10 @@ export default class Pharos {
// * Base case: There is an adjacent sibling at the same depth as the target.
switch (direction) {
- case SiblingSearchDirection.Previous:
- return [eventsAtLevel[targetIndex - 1], parentDTag];
- case SiblingSearchDirection.Next:
- return [eventsAtLevel[targetIndex + 1], parentDTag];
+ case SiblingSearchDirection.Previous:
+ return [eventsAtLevel[targetIndex - 1], parentDTag];
+ case SiblingSearchDirection.Next:
+ return [eventsAtLevel[targetIndex + 1], parentDTag];
}
return [null, null];
@@ -424,7 +439,9 @@ export default class Pharos {
getParent(dTag: string): string | null {
// Check if the event exists in the parser tree.
if (!this.eventIds.has(dTag)) {
- throw new Error(`The event indicated by #d:${dTag} does not exist in the parser tree.`);
+ throw new Error(
+ `The event indicated by #d:${dTag} does not exist in the parser tree.`,
+ );
}
// Iterate through all the index to child mappings.
@@ -449,7 +466,11 @@ export default class Pharos {
* @remarks Moving the target event within the tree changes the hash of several events, so the
* event tree will be regenerated when the consumer next invokes `getEvents()`.
*/
- moveEvent(targetDTag: string, destinationDTag: string, insertAfter: boolean = false): void {
+ moveEvent(
+ targetDTag: string,
+ destinationDTag: string,
+ insertAfter: boolean = false,
+ ): void {
const targetEvent = this.events.get(targetDTag);
const destinationEvent = this.events.get(destinationDTag);
const targetParent = this.getParent(targetDTag);
@@ -464,11 +485,15 @@ export default class Pharos {
}
if (!targetParent) {
- throw new Error(`The event indicated by #d:${targetDTag} does not have a parent.`);
+ throw new Error(
+ `The event indicated by #d:${targetDTag} does not have a parent.`,
+ );
}
if (!destinationParent) {
- throw new Error(`The event indicated by #d:${destinationDTag} does not have a parent.`);
+ throw new Error(
+ `The event indicated by #d:${destinationDTag} does not have a parent.`,
+ );
}
// Remove the target from among the children of its current parent.
@@ -478,16 +503,22 @@ export default class Pharos {
this.indexToChildEventsMap.get(destinationParent)?.delete(targetDTag);
// Get the index of the destination event among the children of its parent.
- const destinationIndex = Array.from(this.indexToChildEventsMap.get(destinationParent) ?? [])
- .indexOf(destinationDTag);
+ const destinationIndex = Array.from(
+ this.indexToChildEventsMap.get(destinationParent) ?? [],
+ ).indexOf(destinationDTag);
// Insert next to the index of the destination event, either before or after as specified by
// the insertAfter flag.
- const destinationChildren = Array.from(this.indexToChildEventsMap.get(destinationParent) ?? []);
+ const destinationChildren = Array.from(
+ this.indexToChildEventsMap.get(destinationParent) ?? [],
+ );
insertAfter
? destinationChildren.splice(destinationIndex + 1, 0, targetDTag)
: destinationChildren.splice(destinationIndex, 0, targetDTag);
- this.indexToChildEventsMap.set(destinationParent, new Set(destinationChildren));
+ this.indexToChildEventsMap.set(
+ destinationParent,
+ new Set(destinationChildren),
+ );
this.shouldUpdateEventTree = true;
}
@@ -517,7 +548,10 @@ export default class Pharos {
* - Each node ID is mapped to an integer event kind that will be used to represent the node.
* - Each ID of a node containing children is mapped to the set of IDs of its children.
*/
- private treeProcessor(treeProcessor: Extensions.TreeProcessor, document: Document) {
+ private treeProcessor(
+ treeProcessor: Extensions.TreeProcessor,
+ document: Document,
+ ) {
this.rootNodeId = this.generateNodeId(document);
document.setId(this.rootNodeId);
this.nodes.set(this.rootNodeId, document);
@@ -533,7 +567,7 @@ export default class Pharos {
continue;
}
- if (block.getContext() === 'section') {
+ if (block.getContext() === "section") {
const children = this.processSection(block as Section);
nodeQueue.push(...children);
} else {
@@ -563,7 +597,7 @@ export default class Pharos {
}
this.nodes.set(sectionId, section);
- this.eventToKindMap.set(sectionId, 30040); // Sections are indexToChildEventsMap by default.
+ this.eventToKindMap.set(sectionId, 30040); // Sections are indexToChildEventsMap by default.
this.indexToChildEventsMap.set(sectionId, new Set());
const parentId = this.normalizeId(section.getParent()?.getId());
@@ -591,7 +625,7 @@ export default class Pharos {
// Obtain or generate a unique ID for the block.
let blockId = this.normalizeId(block.getId());
if (!blockId) {
- blockId = this.generateNodeId(block) ;
+ blockId = this.generateNodeId(block);
block.setId(blockId);
}
@@ -601,7 +635,7 @@ export default class Pharos {
}
this.nodes.set(blockId, block);
- this.eventToKindMap.set(blockId, 30041); // Blocks are zettels by default.
+ this.eventToKindMap.set(blockId, 30041); // Blocks are zettels by default.
const parentId = this.normalizeId(block.getParent()?.getId());
if (!parentId) {
@@ -648,21 +682,24 @@ export default class Pharos {
* @remarks This function does a depth-first crawl of the event tree using the relays specified
* on the NDK instance.
*/
- private async getPublicationContent(event: NDKEvent, depth: number = 0): Promise {
- let content: string = '';
+ private async getPublicationContent(
+ event: NDKEvent,
+ depth: number = 0,
+ ): Promise {
+ let content: string = "";
// Format title into AsciiDoc header.
- const title = getMatchingTags(event, 'title')[0][1];
- let titleLevel = '';
+ const title = getMatchingTags(event, "title")[0][1];
+ let titleLevel = "";
for (let i = 0; i <= depth; i++) {
- titleLevel += '=';
+ titleLevel += "=";
}
content += `${titleLevel} ${title}\n\n`;
// TODO: Deprecate `e` tags in favor of `a` tags required by NIP-62.
- let tags = getMatchingTags(event, 'a');
+ let tags = getMatchingTags(event, "a");
if (tags.length === 0) {
- tags = getMatchingTags(event, 'e');
+ tags = getMatchingTags(event, "e");
}
// Base case: The event is a zettel.
@@ -673,24 +710,29 @@ export default class Pharos {
// Recursive case: The event is an index.
const childEvents = await Promise.all(
- tags.map(tag => this.ndk.fetchEventFromTag(tag, event))
+ tags.map((tag) => this.ndk.fetchEventFromTag(tag, event)),
);
// if a blog, save complete events for later
- if (getMatchingTags(event, 'type').length > 0 && getMatchingTags(event, 'type')[0][1] === 'blog') {
- childEvents.forEach(child => {
+ if (
+ getMatchingTags(event, "type").length > 0 &&
+ getMatchingTags(event, "type")[0][1] === "blog"
+ ) {
+ childEvents.forEach((child) => {
if (child) {
- this.blogEntries.set(getMatchingTags(child, 'd')?.[0]?.[1], child);
+ this.blogEntries.set(getMatchingTags(child, "d")?.[0]?.[1], child);
}
- })
+ });
}
// populate metadata
if (event.created_at) {
- this.rootIndexMetadata.publicationDate = new Date(event.created_at * 1000).toDateString();
+ this.rootIndexMetadata.publicationDate = new Date(
+ event.created_at * 1000,
+ ).toDateString();
}
- if (getMatchingTags(event, 'image').length > 0) {
- this.rootIndexMetadata.coverImage = getMatchingTags(event, 'image')[0][1];
+ if (getMatchingTags(event, "image").length > 0) {
+ this.rootIndexMetadata.coverImage = getMatchingTags(event, "image")[0][1];
}
// Michael J - 15 December 2024 - This could be further parallelized by recursively fetching
@@ -699,17 +741,19 @@ export default class Pharos {
const childContentPromises: Promise[] = [];
for (let i = 0; i < childEvents.length; i++) {
const childEvent = childEvents[i];
-
+
if (!childEvent) {
console.warn(`NDK could not find event ${tags[i][1]}.`);
continue;
}
- childContentPromises.push(this.getPublicationContent(childEvent, depth + 1));
+ childContentPromises.push(
+ this.getPublicationContent(childEvent, depth + 1),
+ );
}
const childContents = await Promise.all(childContentPromises);
- content += childContents.join('\n\n');
+ content += childContents.join("\n\n");
return content;
}
@@ -754,17 +798,17 @@ export default class Pharos {
while (nodeIdStack.length > 0) {
const nodeId = nodeIdStack.pop();
-
- switch (this.eventToKindMap.get(nodeId!)) {
- case 30040:
- events.push(this.generateIndexEvent(nodeId!, pubkey));
- break;
- case 30041:
- default:
- // Kind 30041 (zettel) is currently the default kind for contentful events.
- events.push(this.generateZettelEvent(nodeId!, pubkey));
- break;
+ switch (this.eventToKindMap.get(nodeId!)) {
+ case 30040:
+ events.push(this.generateIndexEvent(nodeId!, pubkey));
+ break;
+
+ case 30041:
+ default:
+ // Kind 30041 (zettel) is currently the default kind for contentful events.
+ events.push(this.generateZettelEvent(nodeId!, pubkey));
+ break;
}
}
@@ -783,17 +827,14 @@ export default class Pharos {
private generateIndexEvent(nodeId: string, pubkey: string): NDKEvent {
const title = (this.nodes.get(nodeId)! as AbstractBlock).getTitle();
// TODO: Use a tags as per NIP-62.
- const childTags = Array.from(this.indexToChildEventsMap.get(nodeId)!)
- .map(id => ['#e', this.eventIds.get(id)!]);
+ const childTags = Array.from(this.indexToChildEventsMap.get(nodeId)!).map(
+ (id) => ["#e", this.eventIds.get(id)!],
+ );
const event = new NDKEvent(this.ndk);
event.kind = 30040;
- event.content = '';
- event.tags = [
- ['title', title!],
- ['#d', nodeId],
- ...childTags
- ];
+ event.content = "";
+ event.tags = [["title", title!], ["#d", nodeId], ...childTags];
event.created_at = Date.now();
event.pubkey = pubkey;
@@ -805,7 +846,7 @@ export default class Pharos {
this.rootIndexMetadata = {
authors: document
.getAuthors()
- .map(author => author.getName())
+ .map((author) => author.getName())
.filter((name): name is string => name != null),
version: document.getRevisionNumber(),
edition: document.getRevisionRemark(),
@@ -813,11 +854,11 @@ export default class Pharos {
};
if (this.rootIndexMetadata.authors) {
- event.tags.push(['author', ...this.rootIndexMetadata.authors!]);
+ event.tags.push(["author", ...this.rootIndexMetadata.authors!]);
}
if (this.rootIndexMetadata.version || this.rootIndexMetadata.edition) {
- const versionTags: string[] = ['version'];
+ const versionTags: string[] = ["version"];
if (this.rootIndexMetadata.version) {
versionTags.push(this.rootIndexMetadata.version);
}
@@ -828,12 +869,15 @@ export default class Pharos {
}
if (this.rootIndexMetadata.publicationDate) {
- event.tags.push(['published_on', this.rootIndexMetadata.publicationDate!]);
+ event.tags.push([
+ "published_on",
+ this.rootIndexMetadata.publicationDate!,
+ ]);
}
}
// Event ID generation must be the last step.
- const eventId = event.getEventHash();
+ const eventId = event.getEventHash();
this.eventIds.set(nodeId, eventId);
event.id = eventId;
@@ -852,21 +896,21 @@ export default class Pharos {
*/
private generateZettelEvent(nodeId: string, pubkey: string): NDKEvent {
const title = (this.nodes.get(nodeId)! as Block).getTitle();
- const content = (this.nodes.get(nodeId)! as Block).getSource(); // AsciiDoc source content.
+ const content = (this.nodes.get(nodeId)! as Block).getSource(); // AsciiDoc source content.
const event = new NDKEvent(this.ndk);
event.kind = 30041;
event.content = content!;
event.tags = [
- ['title', title!],
- ['#d', nodeId],
+ ["title", title!],
+ ["#d", nodeId],
...this.extractAndNormalizeWikilinks(content!),
];
event.created_at = Date.now();
event.pubkey = pubkey;
// Event ID generation must be the last step.
- const eventId = event.getEventHash();
+ const eventId = event.getEventHash();
this.eventIds.set(nodeId, eventId);
event.id = eventId;
@@ -902,173 +946,173 @@ export default class Pharos {
const context = block.getContext();
switch (context) {
- case 'admonition':
- blockNumber = this.contextCounters.get('admonition') ?? 0;
- blockId = `${documentId}-admonition-${blockNumber++}`;
- this.contextCounters.set('admonition', blockNumber);
- break;
+ case "admonition":
+ blockNumber = this.contextCounters.get("admonition") ?? 0;
+ blockId = `${documentId}-admonition-${blockNumber++}`;
+ this.contextCounters.set("admonition", blockNumber);
+ break;
- case 'audio':
- blockNumber = this.contextCounters.get('audio') ?? 0;
- blockId = `${documentId}-audio-${blockNumber++}`;
- this.contextCounters.set('audio', blockNumber);
- break;
+ case "audio":
+ blockNumber = this.contextCounters.get("audio") ?? 0;
+ blockId = `${documentId}-audio-${blockNumber++}`;
+ this.contextCounters.set("audio", blockNumber);
+ break;
- case 'colist':
- blockNumber = this.contextCounters.get('colist') ?? 0;
- blockId = `${documentId}-colist-${blockNumber++}`;
- this.contextCounters.set('colist', blockNumber);
- break;
+ case "colist":
+ blockNumber = this.contextCounters.get("colist") ?? 0;
+ blockId = `${documentId}-colist-${blockNumber++}`;
+ this.contextCounters.set("colist", blockNumber);
+ break;
- case 'dlist':
- blockNumber = this.contextCounters.get('dlist') ?? 0;
- blockId = `${documentId}-dlist-${blockNumber++}`;
- this.contextCounters.set('dlist', blockNumber);
- break;
+ case "dlist":
+ blockNumber = this.contextCounters.get("dlist") ?? 0;
+ blockId = `${documentId}-dlist-${blockNumber++}`;
+ this.contextCounters.set("dlist", blockNumber);
+ break;
- case 'document':
- blockNumber = this.contextCounters.get('document') ?? 0;
- blockId = `${documentId}-document-${blockNumber++}`;
- this.contextCounters.set('document', blockNumber);
- break;
+ case "document":
+ blockNumber = this.contextCounters.get("document") ?? 0;
+ blockId = `${documentId}-document-${blockNumber++}`;
+ this.contextCounters.set("document", blockNumber);
+ break;
- case 'example':
- blockNumber = this.contextCounters.get('example') ?? 0;
- blockId = `${documentId}-example-${blockNumber++}`;
- this.contextCounters.set('example', blockNumber);
- break;
+ case "example":
+ blockNumber = this.contextCounters.get("example") ?? 0;
+ blockId = `${documentId}-example-${blockNumber++}`;
+ this.contextCounters.set("example", blockNumber);
+ break;
- case 'floating_title':
- blockNumber = this.contextCounters.get('floating_title') ?? 0;
- blockId = `${documentId}-floating-title-${blockNumber++}`;
- this.contextCounters.set('floating_title', blockNumber);
- break;
+ case "floating_title":
+ blockNumber = this.contextCounters.get("floating_title") ?? 0;
+ blockId = `${documentId}-floating-title-${blockNumber++}`;
+ this.contextCounters.set("floating_title", blockNumber);
+ break;
- case 'image':
- blockNumber = this.contextCounters.get('image') ?? 0;
- blockId = `${documentId}-image-${blockNumber++}`;
- this.contextCounters.set('image', blockNumber);
- break;
+ case "image":
+ blockNumber = this.contextCounters.get("image") ?? 0;
+ blockId = `${documentId}-image-${blockNumber++}`;
+ this.contextCounters.set("image", blockNumber);
+ break;
- case 'list_item':
- blockNumber = this.contextCounters.get('list_item') ?? 0;
- blockId = `${documentId}-list-item-${blockNumber++}`;
- this.contextCounters.set('list_item', blockNumber);
- break;
+ case "list_item":
+ blockNumber = this.contextCounters.get("list_item") ?? 0;
+ blockId = `${documentId}-list-item-${blockNumber++}`;
+ this.contextCounters.set("list_item", blockNumber);
+ break;
- case 'listing':
- blockNumber = this.contextCounters.get('listing') ?? 0;
- blockId = `${documentId}-listing-${blockNumber++}`;
- this.contextCounters.set('listing', blockNumber);
- break;
+ case "listing":
+ blockNumber = this.contextCounters.get("listing") ?? 0;
+ blockId = `${documentId}-listing-${blockNumber++}`;
+ this.contextCounters.set("listing", blockNumber);
+ break;
- case 'literal':
- blockNumber = this.contextCounters.get('literal') ?? 0;
- blockId = `${documentId}-literal-${blockNumber++}`;
- this.contextCounters.set('literal', blockNumber);
- break;
+ case "literal":
+ blockNumber = this.contextCounters.get("literal") ?? 0;
+ blockId = `${documentId}-literal-${blockNumber++}`;
+ this.contextCounters.set("literal", blockNumber);
+ break;
- case 'olist':
- blockNumber = this.contextCounters.get('olist') ?? 0;
- blockId = `${documentId}-olist-${blockNumber++}`;
- this.contextCounters.set('olist', blockNumber);
- break;
+ case "olist":
+ blockNumber = this.contextCounters.get("olist") ?? 0;
+ blockId = `${documentId}-olist-${blockNumber++}`;
+ this.contextCounters.set("olist", blockNumber);
+ break;
- case 'open':
- blockNumber = this.contextCounters.get('open') ?? 0;
- blockId = `${documentId}-open-${blockNumber++}`;
- this.contextCounters.set('open', blockNumber);
- break;
+ case "open":
+ blockNumber = this.contextCounters.get("open") ?? 0;
+ blockId = `${documentId}-open-${blockNumber++}`;
+ this.contextCounters.set("open", blockNumber);
+ break;
- case 'page_break':
- blockNumber = this.contextCounters.get('page_break') ?? 0;
- blockId = `${documentId}-page-break-${blockNumber++}`;
- this.contextCounters.set('page_break', blockNumber);
- break;
+ case "page_break":
+ blockNumber = this.contextCounters.get("page_break") ?? 0;
+ blockId = `${documentId}-page-break-${blockNumber++}`;
+ this.contextCounters.set("page_break", blockNumber);
+ break;
- case 'paragraph':
- blockNumber = this.contextCounters.get('paragraph') ?? 0;
- blockId = `${documentId}-paragraph-${blockNumber++}`;
- this.contextCounters.set('paragraph', blockNumber);
- break;
+ case "paragraph":
+ blockNumber = this.contextCounters.get("paragraph") ?? 0;
+ blockId = `${documentId}-paragraph-${blockNumber++}`;
+ this.contextCounters.set("paragraph", blockNumber);
+ break;
- case 'pass':
- blockNumber = this.contextCounters.get('pass') ?? 0;
- blockId = `${documentId}-pass-${blockNumber++}`;
- this.contextCounters.set('pass', blockNumber);
- break;
+ case "pass":
+ blockNumber = this.contextCounters.get("pass") ?? 0;
+ blockId = `${documentId}-pass-${blockNumber++}`;
+ this.contextCounters.set("pass", blockNumber);
+ break;
- case 'preamble':
- blockNumber = this.contextCounters.get('preamble') ?? 0;
- blockId = `${documentId}-preamble-${blockNumber++}`;
- this.contextCounters.set('preamble', blockNumber);
- break;
+ case "preamble":
+ blockNumber = this.contextCounters.get("preamble") ?? 0;
+ blockId = `${documentId}-preamble-${blockNumber++}`;
+ this.contextCounters.set("preamble", blockNumber);
+ break;
- case 'quote':
- blockNumber = this.contextCounters.get('quote') ?? 0;
- blockId = `${documentId}-quote-${blockNumber++}`;
- this.contextCounters.set('quote', blockNumber);
- break;
+ case "quote":
+ blockNumber = this.contextCounters.get("quote") ?? 0;
+ blockId = `${documentId}-quote-${blockNumber++}`;
+ this.contextCounters.set("quote", blockNumber);
+ break;
- case 'section':
- blockNumber = this.contextCounters.get('section') ?? 0;
- blockId = `${documentId}-section-${blockNumber++}`;
- this.contextCounters.set('section', blockNumber);
- break;
+ case "section":
+ blockNumber = this.contextCounters.get("section") ?? 0;
+ blockId = `${documentId}-section-${blockNumber++}`;
+ this.contextCounters.set("section", blockNumber);
+ break;
- case 'sidebar':
- blockNumber = this.contextCounters.get('sidebar') ?? 0;
- blockId = `${documentId}-sidebar-${blockNumber++}`;
- this.contextCounters.set('sidebar', blockNumber);
- break;
+ case "sidebar":
+ blockNumber = this.contextCounters.get("sidebar") ?? 0;
+ blockId = `${documentId}-sidebar-${blockNumber++}`;
+ this.contextCounters.set("sidebar", blockNumber);
+ break;
- case 'table':
- blockNumber = this.contextCounters.get('table') ?? 0;
- blockId = `${documentId}-table-${blockNumber++}`;
- this.contextCounters.set('table', blockNumber);
- break;
+ case "table":
+ blockNumber = this.contextCounters.get("table") ?? 0;
+ blockId = `${documentId}-table-${blockNumber++}`;
+ this.contextCounters.set("table", blockNumber);
+ break;
- case 'table_cell':
- blockNumber = this.contextCounters.get('table_cell') ?? 0;
- blockId = `${documentId}-table-cell-${blockNumber++}`;
- this.contextCounters.set('table_cell', blockNumber);
- break;
+ case "table_cell":
+ blockNumber = this.contextCounters.get("table_cell") ?? 0;
+ blockId = `${documentId}-table-cell-${blockNumber++}`;
+ this.contextCounters.set("table_cell", blockNumber);
+ break;
- case 'thematic_break':
- blockNumber = this.contextCounters.get('thematic_break') ?? 0;
- blockId = `${documentId}-thematic-break-${blockNumber++}`;
- this.contextCounters.set('thematic_break', blockNumber);
- break;
+ case "thematic_break":
+ blockNumber = this.contextCounters.get("thematic_break") ?? 0;
+ blockId = `${documentId}-thematic-break-${blockNumber++}`;
+ this.contextCounters.set("thematic_break", blockNumber);
+ break;
- case 'toc':
- blockNumber = this.contextCounters.get('toc') ?? 0;
- blockId = `${documentId}-toc-${blockNumber++}`;
- this.contextCounters.set('toc', blockNumber);
- break;
+ case "toc":
+ blockNumber = this.contextCounters.get("toc") ?? 0;
+ blockId = `${documentId}-toc-${blockNumber++}`;
+ this.contextCounters.set("toc", blockNumber);
+ break;
- case 'ulist':
- blockNumber = this.contextCounters.get('ulist') ?? 0;
- blockId = `${documentId}-ulist-${blockNumber++}`;
- this.contextCounters.set('ulist', blockNumber);
- break;
+ case "ulist":
+ blockNumber = this.contextCounters.get("ulist") ?? 0;
+ blockId = `${documentId}-ulist-${blockNumber++}`;
+ this.contextCounters.set("ulist", blockNumber);
+ break;
- case 'verse':
- blockNumber = this.contextCounters.get('verse') ?? 0;
- blockId = `${documentId}-verse-${blockNumber++}`;
- this.contextCounters.set('verse', blockNumber);
- break;
+ case "verse":
+ blockNumber = this.contextCounters.get("verse") ?? 0;
+ blockId = `${documentId}-verse-${blockNumber++}`;
+ this.contextCounters.set("verse", blockNumber);
+ break;
- case 'video':
- blockNumber = this.contextCounters.get('video') ?? 0;
- blockId = `${documentId}-video-${blockNumber++}`;
- this.contextCounters.set('video', blockNumber);
- break;
+ case "video":
+ blockNumber = this.contextCounters.get("video") ?? 0;
+ blockId = `${documentId}-video-${blockNumber++}`;
+ this.contextCounters.set("video", blockNumber);
+ break;
- default:
- blockNumber = this.contextCounters.get('block') ?? 0;
- blockId = `${documentId}-block-${blockNumber++}`;
- this.contextCounters.set('block', blockNumber);
- break;
+ default:
+ blockNumber = this.contextCounters.get("block") ?? 0;
+ blockId = `${documentId}-block-${blockNumber++}`;
+ this.contextCounters.set("block", blockNumber);
+ break;
}
block.setId(blockId);
@@ -1082,24 +1126,25 @@ export default class Pharos {
return null;
}
- return he.decode(input)
+ return he
+ .decode(input)
.toLowerCase()
- .replace(/[_]/g, ' ') // Replace underscores with spaces.
+ .replace(/[_]/g, " ") // Replace underscores with spaces.
.trim()
- .replace(/\s+/g, '-') // Replace spaces with dashes.
- .replace(/[^a-z0-9\-]/g, ''); // Remove non-alphanumeric characters except dashes.
+ .replace(/\s+/g, "-") // Replace spaces with dashes.
+ .replace(/[^a-z0-9\-]/g, ""); // Remove non-alphanumeric characters except dashes.
}
private updateEventByContext(dTag: string, value: string, context: string) {
switch (context) {
- case 'document':
- case 'section':
- this.updateEventTitle(dTag, value);
- break;
-
- default:
- this.updateEventBody(dTag, value);
- break;
+ case "document":
+ case "section":
+ this.updateEventTitle(dTag, value);
+ break;
+
+ default:
+ this.updateEventBody(dTag, value);
+ break;
}
}
@@ -1131,7 +1176,7 @@ export default class Pharos {
while ((match = wikilinkPattern.exec(content)) !== null) {
const linkName = match[1];
const normalizedText = this.normalizeId(linkName);
- wikilinks.push(['wikilink', normalizedText!]);
+ wikilinks.push(["wikilink", normalizedText!]);
}
return wikilinks;
@@ -1147,7 +1192,7 @@ export const pharosInstance: Writable = writable();
export const tocUpdate = writable(0);
// Whenever you update the publication tree, call:
-tocUpdate.update(n => n + 1);
+tocUpdate.update((n) => n + 1);
function ensureAsciiDocHeader(content: string): string {
const lines = content.split(/\r?\n/);
@@ -1156,35 +1201,36 @@ function ensureAsciiDocHeader(content: string): string {
// Find the first non-empty line as header
for (let i = 0; i < lines.length; i++) {
- if (lines[i].trim() === '') continue;
- if (lines[i].trim().startsWith('=')) {
+ if (lines[i].trim() === "") continue;
+ if (lines[i].trim().startsWith("=")) {
headerIndex = i;
break;
} else {
- throw new Error('AsciiDoc document is missing a header at the top.');
+ throw new Error("AsciiDoc document is missing a header at the top.");
}
}
if (headerIndex === -1) {
- throw new Error('AsciiDoc document is missing a header.');
+ throw new Error("AsciiDoc document is missing a header.");
}
// Check for doctype in the next non-empty line after header
let nextLine = headerIndex + 1;
- while (nextLine < lines.length && lines[nextLine].trim() === '') {
+ while (nextLine < lines.length && lines[nextLine].trim() === "") {
nextLine++;
}
- if (nextLine < lines.length && lines[nextLine].trim().startsWith(':doctype:')) {
+ if (
+ nextLine < lines.length &&
+ lines[nextLine].trim().startsWith(":doctype:")
+ ) {
hasDoctype = true;
}
// Insert doctype immediately after header if not present
if (!hasDoctype) {
- lines.splice(headerIndex + 1, 0, ':doctype: book');
+ lines.splice(headerIndex + 1, 0, ":doctype: book");
}
-
-
- return lines.join('\n');
+ return lines.join("\n");
}
diff --git a/src/lib/snippets/PublicationSnippets.svelte b/src/lib/snippets/PublicationSnippets.svelte
index 802edfd..3687062 100644
--- a/src/lib/snippets/PublicationSnippets.svelte
+++ b/src/lib/snippets/PublicationSnippets.svelte
@@ -1,5 +1,5 @@
-
@@ -8,13 +8,17 @@
{@const headingLevel = Math.min(depth + 1, 6)}
-
+
{title}
{/snippet}
-{#snippet contentParagraph(content: string, publicationType: string, isSectionStart: boolean)}
-
+{#snippet contentParagraph(
+ content: string,
+ publicationType: string,
+ isSectionStart: boolean,
+)}
+
{@html content}
{/snippet}
diff --git a/src/lib/snippets/UserSnippets.svelte b/src/lib/snippets/UserSnippets.svelte
index d8c960e..8a0774b 100644
--- a/src/lib/snippets/UserSnippets.svelte
+++ b/src/lib/snippets/UserSnippets.svelte
@@ -1,5 +1,9 @@
-
@@ -14,6 +18,6 @@
{@html createProfileLink(toNpub(identifier) as string, displayText)}
{/await}
{:else}
- {displayText ?? ''}
+ {displayText ?? ""}
{/if}
{/snippet}
diff --git a/src/lib/stores.ts b/src/lib/stores.ts
index e38f0d4..74219db 100644
--- a/src/lib/stores.ts
+++ b/src/lib/stores.ts
@@ -7,14 +7,13 @@ export let alexandriaKinds = readable([30040, 30041, 30818]);
export let feedType = writable(FeedType.StandardRelays);
-
const defaultVisibility = {
toc: false,
blog: true,
main: true,
inner: false,
discussion: false,
- editing: false
+ editing: false,
};
function createVisibilityStore() {
@@ -24,7 +23,7 @@ function createVisibilityStore() {
subscribe,
set,
update,
- reset: () => set({ ...defaultVisibility })
+ reset: () => set({ ...defaultVisibility }),
};
}
diff --git a/src/lib/stores/relayStore.ts b/src/lib/stores/relayStore.ts
index 9c7e635..2c038c7 100644
--- a/src/lib/stores/relayStore.ts
+++ b/src/lib/stores/relayStore.ts
@@ -1,4 +1,4 @@
-import { writable } from 'svelte/store';
+import { writable } from "svelte/store";
// Initialize with empty array, will be populated from user preferences
-export const userRelays = writable([]);
\ No newline at end of file
+export const userRelays = writable([]);
diff --git a/src/lib/types.ts b/src/lib/types.ts
index 9b8e84e..e47b037 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -6,4 +6,10 @@ export type Tab = {
data?: any;
};
-export type TabType = 'welcome' | 'find' | 'article' | 'user' | 'settings' | 'editor';
+export type TabType =
+ | "welcome"
+ | "find"
+ | "article"
+ | "user"
+ | "settings"
+ | "editor";
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index 52f5686..b5be33c 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -12,11 +12,11 @@ export function neventEncode(event: NDKEvent, relays: string[]) {
}
export function naddrEncode(event: NDKEvent, relays: string[]) {
- const dTag = getMatchingTags(event, 'd')[0]?.[1];
+ const dTag = getMatchingTags(event, "d")[0]?.[1];
if (!dTag) {
- throw new Error('Event does not have a d tag');
+ throw new Error("Event does not have a d tag");
}
-
+
return nip19.naddrEncode({
identifier: dTag,
pubkey: event.pubkey,
@@ -110,16 +110,14 @@ export function isElementInViewport(el: string | HTMLElement) {
export function filterValidIndexEvents(events: Set): Set {
// The filter object supports only limited parameters, so we need to filter out events that
// don't respect NKBIP-01.
- events.forEach(event => {
+ events.forEach((event) => {
// Index events have no content, and they must have `title`, `d`, and `e` tags.
if (
- (event.content != null && event.content.length > 0)
- || getMatchingTags(event, 'title').length === 0
- || getMatchingTags(event, 'd').length === 0
- || (
- getMatchingTags(event, 'a').length === 0
- && getMatchingTags(event, 'e').length === 0
- )
+ (event.content != null && event.content.length > 0) ||
+ getMatchingTags(event, "title").length === 0 ||
+ getMatchingTags(event, "d").length === 0 ||
+ (getMatchingTags(event, "a").length === 0 &&
+ getMatchingTags(event, "e").length === 0)
) {
events.delete(event);
}
@@ -138,7 +136,7 @@ export function filterValidIndexEvents(events: Set): Set {
*/
export async function findIndexAsync(
array: T[],
- predicate: (element: T, index: number, array: T[]) => Promise
+ predicate: (element: T, index: number, array: T[]) => Promise,
): Promise {
for (let i = 0; i < array.length; i++) {
if (await predicate(array[i], i, array)) {
@@ -152,14 +150,14 @@ export async function findIndexAsync(
declare global {
interface Array {
findIndexAsync(
- predicate: (element: T, index: number, array: T[]) => Promise
+ predicate: (element: T, index: number, array: T[]) => Promise,
): Promise;
}
}
-Array.prototype.findIndexAsync = function(
+Array.prototype.findIndexAsync = function (
this: T[],
- predicate: (element: T, index: number, array: T[]) => Promise
+ predicate: (element: T, index: number, array: T[]) => Promise,
): Promise {
return findIndexAsync(this, predicate);
};
@@ -173,7 +171,7 @@ Array.prototype.findIndexAsync = function(
*/
export function debounce any>(
func: T,
- wait: number
+ wait: number,
): (...args: Parameters) => void {
let timeout: ReturnType | undefined;
diff --git a/src/lib/utils/markup/MarkupInfo.md b/src/lib/utils/markup/MarkupInfo.md
index 22a108f..38d78e6 100644
--- a/src/lib/utils/markup/MarkupInfo.md
+++ b/src/lib/utils/markup/MarkupInfo.md
@@ -6,8 +6,8 @@ Alexandria supports multiple markup formats for different use cases. Below is a
The **basic markup parser** follows the [Nostr best-practice guidelines](https://github.com/nostrability/nostrability/issues/146) and supports:
-- **Headers:**
- - ATX-style: `# H1` through `###### H6`
+- **Headers:**
+ - ATX-style: `# H1` through `###### H6`
- Setext-style: `H1\n=====`
- **Bold:** `*bold*` or `**bold**`
- **Italic:** `_italic_` or `__italic__`
@@ -123,7 +123,8 @@ For more information on AsciiDoc, see the [AsciiDoc documentation](https://ascii
---
**Note:**
+
- The markdown parsers are primarily used for comments, issues, and other user-generated content.
- Publications and wikis are rendered using AsciiDoc for maximum expressiveness and compatibility.
- All URLs are sanitized to remove tracking parameters, and YouTube links are presented in a clean, privacy-friendly format.
-- [Here is a test markup file](/tests/integration/markupTestfile.md) that you can use to test out the parser and see how things should be formatted.
\ No newline at end of file
+- [Here is a test markup file](/tests/integration/markupTestfile.md) that you can use to test out the parser and see how things should be formatted.
diff --git a/src/lib/utils/markup/advancedAsciidoctorPostProcessor.ts b/src/lib/utils/markup/advancedAsciidoctorPostProcessor.ts
index 36657f9..0700496 100644
--- a/src/lib/utils/markup/advancedAsciidoctorPostProcessor.ts
+++ b/src/lib/utils/markup/advancedAsciidoctorPostProcessor.ts
@@ -1,14 +1,16 @@
-import { postProcessAsciidoctorHtml } from './asciidoctorPostProcessor';
-import plantumlEncoder from 'plantuml-encoder';
+import { postProcessAsciidoctorHtml } from "./asciidoctorPostProcessor";
+import plantumlEncoder from "plantuml-encoder";
/**
* Unified post-processor for Asciidoctor HTML that handles:
* - Math rendering (Asciimath/Latex, stem blocks)
* - PlantUML diagrams
- * - BPMN diagrams
+ * - BPMN diagrams
* - TikZ diagrams
*/
-export async function postProcessAdvancedAsciidoctorHtml(html: string): Promise {
+export async function postProcessAdvancedAsciidoctorHtml(
+ html: string,
+): Promise {
if (!html) return html;
try {
// First apply the basic post-processing (wikilinks, nostr addresses)
@@ -22,15 +24,21 @@ export async function postProcessAdvancedAsciidoctorHtml(html: string): Promise<
// Process TikZ blocks
processedHtml = processTikZBlocks(processedHtml);
// After all processing, apply highlight.js if available
- if (typeof window !== 'undefined' && typeof window.hljs?.highlightAll === 'function') {
+ if (
+ typeof window !== "undefined" &&
+ typeof window.hljs?.highlightAll === "function"
+ ) {
setTimeout(() => window.hljs!.highlightAll(), 0);
}
- if (typeof window !== 'undefined' && typeof (window as any).MathJax?.typesetPromise === 'function') {
+ if (
+ typeof window !== "undefined" &&
+ typeof (window as any).MathJax?.typesetPromise === "function"
+ ) {
setTimeout(() => (window as any).MathJax.typesetPromise(), 0);
}
return processedHtml;
} catch (error) {
- console.error('Error in postProcessAdvancedAsciidoctorHtml:', error);
+ console.error("Error in postProcessAdvancedAsciidoctorHtml:", error);
return html; // Return original HTML if processing fails
}
}
@@ -41,44 +49,46 @@ export async function postProcessAdvancedAsciidoctorHtml(html: string): Promise<
*/
function fixAllMathBlocks(html: string): string {
// Unescape \$ to $ for math delimiters
- html = html.replace(/\\\$/g, '$');
-
-
+ html = html.replace(/\\\$/g, "$");
// Block math:
...
html = html.replace(
/
\s*
([\s\S]*?)<\/div>\s*<\/div>/g,
(_match, mathContent) => {
let cleanMath = mathContent
- .replace(/\$<\/span>/g, '')
- .replace(/\$\$<\/span>/g, '')
+ .replace(/\$<\/span>/g, "")
+ .replace(/\$\$<\/span>/g, "")
// Remove $ or $$ on their own line, or surrounded by whitespace/newlines
- .replace(/(^|[\n\r\s])\$([\n\r\s]|$)/g, '$1$2')
- .replace(/(^|[\n\r\s])\$\$([\n\r\s]|$)/g, '$1$2')
+ .replace(/(^|[\n\r\s])\$([\n\r\s]|$)/g, "$1$2")
+ .replace(/(^|[\n\r\s])\$\$([\n\r\s]|$)/g, "$1$2")
// Remove all leading and trailing whitespace and $
- .replace(/^[\s$]+/, '').replace(/[\s$]+$/, '')
+ .replace(/^[\s$]+/, "")
+ .replace(/[\s$]+$/, "")
.trim(); // Final trim to remove any stray whitespace or $
// Always wrap in $$...$$
return `
@@ -283,12 +302,12 @@ function processTikZBlocks(html: string): string {
`;
} catch (error) {
- console.warn('Failed to process TikZ fallback block:', error);
+ console.warn("Failed to process TikZ fallback block:", error);
return match;
}
}
return match;
- }
+ },
);
return html;
}
@@ -297,7 +316,7 @@ function processTikZBlocks(html: string): string {
* Escapes HTML characters for safe display
*/
function escapeHtml(text: string): string {
- const div = document.createElement('div');
+ const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
-}
\ No newline at end of file
+}
diff --git a/src/lib/utils/markup/advancedMarkupParser.ts b/src/lib/utils/markup/advancedMarkupParser.ts
index 9273857..34785ba 100644
--- a/src/lib/utils/markup/advancedMarkupParser.ts
+++ b/src/lib/utils/markup/advancedMarkupParser.ts
@@ -1,11 +1,11 @@
-import { parseBasicmarkup } from './basicMarkupParser';
-import hljs from 'highlight.js';
-import 'highlight.js/lib/common'; // Import common languages
-import 'highlight.js/styles/github-dark.css'; // Dark theme only
+import { parseBasicmarkup } from "./basicMarkupParser";
+import hljs from "highlight.js";
+import "highlight.js/lib/common"; // Import common languages
+import "highlight.js/styles/github-dark.css"; // Dark theme only
// Register common languages
hljs.configure({
- ignoreUnescapedHTML: true
+ ignoreUnescapedHTML: true,
});
// Regular expressions for advanced markup elements
@@ -17,18 +17,28 @@ const FOOTNOTE_REFERENCE_REGEX = /\[\^([^\]]+)\]/g;
const FOOTNOTE_DEFINITION_REGEX = /^\[\^([^\]]+)\]:\s*(.+)$/gm;
const CODE_BLOCK_REGEX = /^```(\w*)$/;
+// LaTeX math regex patterns
+const INLINE_MATH_REGEX = /\$([^$\n]+?)\$/g;
+const DISPLAY_MATH_REGEX = /\$\$([\s\S]*?)\$\$/g;
+const LATEX_BLOCK_REGEX = /\\\[([\s\S]*?)\\\]/g;
+const LATEX_INLINE_REGEX = /\\\(([^)]+?)\\\)/g;
+// Add regex for LaTeX display math environments (e.g., \begin{pmatrix}...\end{pmatrix})
+// Improved regex: match optional whitespace/linebreaks before and after, and allow for indented environments
+const LATEX_ENV_BLOCK_REGEX =
+ /(?:^|\n)\s*\\begin\{([a-zA-Z*]+)\}([\s\S]*?)\\end\{\1\}\s*(?=\n|$)/gm;
+
/**
* Process headings (both styles)
*/
function processHeadings(content: string): string {
// Tailwind classes for each heading level
const headingClasses = [
- 'text-4xl font-bold mt-6 mb-4 text-gray-800 dark:text-gray-300', // h1
- 'text-3xl font-bold mt-6 mb-4 text-gray-800 dark:text-gray-300', // h2
- 'text-2xl font-bold mt-6 mb-4 text-gray-800 dark:text-gray-300', // h3
- 'text-xl font-bold mt-6 mb-4 text-gray-800 dark:text-gray-300', // h4
- 'text-lg font-semibold mt-6 mb-4 text-gray-800 dark:text-gray-300', // h5
- 'text-base font-semibold mt-6 mb-4 text-gray-800 dark:text-gray-300', // h6
+ "text-4xl font-bold mt-6 mb-4 text-gray-800 dark:text-gray-300", // h1
+ "text-3xl font-bold mt-6 mb-4 text-gray-800 dark:text-gray-300", // h2
+ "text-2xl font-bold mt-6 mb-4 text-gray-800 dark:text-gray-300", // h3
+ "text-xl font-bold mt-6 mb-4 text-gray-800 dark:text-gray-300", // h4
+ "text-lg font-semibold mt-6 mb-4 text-gray-800 dark:text-gray-300", // h5
+ "text-base font-semibold mt-6 mb-4 text-gray-800 dark:text-gray-300", // h6
];
// Process ATX-style headings (# Heading)
@@ -39,11 +49,14 @@ function processHeadings(content: string): string {
});
// Process Setext-style headings (Heading\n====)
- processedContent = processedContent.replace(ALTERNATE_HEADING_REGEX, (_, text, level) => {
- const headingLevel = level[0] === '=' ? 1 : 2;
- const classes = headingClasses[headingLevel - 1];
- return `${text.trim()}`;
- });
+ processedContent = processedContent.replace(
+ ALTERNATE_HEADING_REGEX,
+ (_, text, level) => {
+ const headingLevel = level[0] === "=" ? 1 : 2;
+ const classes = headingClasses[headingLevel - 1];
+ return `${text.trim()}`;
+ },
+ );
return processedContent;
}
@@ -53,29 +66,30 @@ function processHeadings(content: string): string {
*/
function processTables(content: string): string {
try {
- if (!content) return '';
-
+ if (!content) return "";
+
return content.replace(/^\|(.*(?:\n\|.*)*)/gm, (match) => {
try {
// Split into rows and clean up
- const rows = match.split('\n').filter(row => row.trim());
+ const rows = match.split("\n").filter((row) => row.trim());
if (rows.length < 1) return match;
// Helper to process a row into cells
const processCells = (row: string): string[] => {
return row
- .split('|')
+ .split("|")
.slice(1, -1) // Remove empty cells from start/end
- .map(cell => cell.trim());
+ .map((cell) => cell.trim());
};
// Check if second row is a delimiter row (only hyphens)
- const hasHeader = rows.length > 1 && rows[1].trim().match(/^\|[-\s|]+\|$/);
-
+ const hasHeader =
+ rows.length > 1 && rows[1].trim().match(/^\|[-\s|]+\|$/);
+
// Extract header and body rows
let headerCells: string[] = [];
let bodyRows: string[] = [];
-
+
if (hasHeader) {
// If we have a header, first row is header, skip delimiter, rest is body
headerCells = processCells(rows[0]);
@@ -91,33 +105,33 @@ function processTables(content: string): string {
// Add header if exists
if (hasHeader) {
- html += '\n
\n';
- headerCells.forEach(cell => {
+ html += "\n
\n";
+ headerCells.forEach((cell) => {
html += `
${cell}
\n`;
});
- html += '
\n\n';
+ html += "
\n\n";
}
// Add body
- html += '\n';
- bodyRows.forEach(row => {
+ html += "\n";
+ bodyRows.forEach((row) => {
const cells = processCells(row);
- html += '
\n';
- cells.forEach(cell => {
+ html += "
\n";
+ cells.forEach((cell) => {
html += `
${cell}
\n`;
});
- html += '
\n';
+ html += "\n";
});
- html += '\n\n
';
+ html += "\n\n
";
return html;
} catch (e: unknown) {
- console.error('Error processing table row:', e);
+ console.error("Error processing table row:", e);
return match;
}
});
} catch (e: unknown) {
- console.error('Error in processTables:', e);
+ console.error("Error in processTables:", e);
return content;
}
}
@@ -126,8 +140,9 @@ function processTables(content: string): string {
* Process horizontal rules
*/
function processHorizontalRules(content: string): string {
- return content.replace(HORIZONTAL_RULE_REGEX,
- ''
+ return content.replace(
+ HORIZONTAL_RULE_REGEX,
+ '',
);
}
@@ -136,7 +151,7 @@ function processHorizontalRules(content: string): string {
*/
function processFootnotes(content: string): string {
try {
- if (!content) return '';
+ if (!content) return "";
// Collect all footnote definitions (but do not remove them from the text yet)
const footnotes = new Map();
@@ -146,48 +161,57 @@ function processFootnotes(content: string): string {
});
// Remove all footnote definition lines from the main content
- let processedContent = content.replace(FOOTNOTE_DEFINITION_REGEX, '');
+ let processedContent = content.replace(FOOTNOTE_DEFINITION_REGEX, "");
// Track all references to each footnote
- const referenceOrder: { id: string, refNum: number, label: string }[] = [];
+ const referenceOrder: { id: string; refNum: number; label: string }[] = [];
const referenceMap = new Map(); // id -> [refNum, ...]
let globalRefNum = 1;
- processedContent = processedContent.replace(FOOTNOTE_REFERENCE_REGEX, (match, id) => {
- if (!footnotes.has(id)) {
- console.warn(`Footnote reference [^${id}] found but no definition exists`);
- return match;
- }
- const refNum = globalRefNum++;
- if (!referenceMap.has(id)) referenceMap.set(id, []);
- referenceMap.get(id)!.push(refNum);
- referenceOrder.push({ id, refNum, label: id });
- return `[${refNum}]`;
- });
+ processedContent = processedContent.replace(
+ FOOTNOTE_REFERENCE_REGEX,
+ (match, id) => {
+ if (!footnotes.has(id)) {
+ console.warn(
+ `Footnote reference [^${id}] found but no definition exists`,
+ );
+ return match;
+ }
+ const refNum = globalRefNum++;
+ if (!referenceMap.has(id)) referenceMap.set(id, []);
+ referenceMap.get(id)!.push(refNum);
+ referenceOrder.push({ id, refNum, label: id });
+ return `[${refNum}]`;
+ },
+ );
// Only render footnotes section if there are actual definitions and at least one reference
if (footnotes.size > 0 && referenceOrder.length > 0) {
- processedContent += '\n\n
Footnotes
\n\n';
+ processedContent +=
+ '\n\n
Footnotes
\n\n';
// Only include each unique footnote once, in order of first reference
const seen = new Set();
for (const { id, label } of referenceOrder) {
if (seen.has(id)) continue;
seen.add(id);
- const text = footnotes.get(id) || '';
+ const text = footnotes.get(id) || "";
// List of backrefs for this footnote
const refs = referenceMap.get(id) || [];
- const backrefs = refs.map((num, i) =>
- `↩${num}`
- ).join(' ');
+ const backrefs = refs
+ .map(
+ (num, i) =>
+ `↩${num}`,
+ )
+ .join(" ");
// If label is not a number, show it after all backrefs
- const labelSuffix = isNaN(Number(label)) ? ` ${label}` : '';
+ const labelSuffix = isNaN(Number(label)) ? ` ${label}` : "";
processedContent += `
${text} ${backrefs}${labelSuffix}
\n`;
}
- processedContent += '';
+ processedContent += "";
}
return processedContent;
} catch (error) {
- console.error('Error processing footnotes:', error);
+ console.error("Error processing footnotes:", error);
return content;
}
}
@@ -198,15 +222,15 @@ function processFootnotes(content: string): string {
function processBlockquotes(content: string): string {
// Match blockquotes that might span multiple lines
const blockquoteRegex = /^>[ \t]?(.+(?:\n>[ \t]?.+)*)/gm;
-
+
return content.replace(blockquoteRegex, (match) => {
// Remove the '>' prefix from each line and preserve line breaks
const text = match
- .split('\n')
- .map(line => line.replace(/^>[ \t]?/, ''))
- .join('\n')
+ .split("\n")
+ .map((line) => line.replace(/^>[ \t]?/, ""))
+ .join("\n")
.trim();
-
+
return `
${text}
`;
});
}
@@ -214,20 +238,23 @@ function processBlockquotes(content: string): string {
/**
* Process code blocks by finding consecutive code lines and preserving their content
*/
-function processCodeBlocks(text: string): { text: string; blocks: Map } {
- const lines = text.split('\n');
+function processCodeBlocks(text: string): {
+ text: string;
+ blocks: Map;
+} {
+ const lines = text.split("\n");
const processedLines: string[] = [];
const blocks = new Map();
let inCodeBlock = false;
let currentCode: string[] = [];
- let currentLanguage = '';
+ let currentLanguage = "";
let blockCount = 0;
let lastWasCodeBlock = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const codeBlockStart = line.match(CODE_BLOCK_REGEX);
-
+
if (codeBlockStart) {
if (!inCodeBlock) {
// Starting a new code block
@@ -239,36 +266,39 @@ function processCodeBlocks(text: string): { text: string; blocks: Map 0) {
blockCount++;
const id = `CODE_BLOCK_${blockCount}`;
- const code = currentCode.join('\n');
-
+ const code = currentCode.join("\n");
+
// Try to format JSON if specified
let formattedCode = code;
- if (currentLanguage.toLowerCase() === 'json') {
+ if (currentLanguage.toLowerCase() === "json") {
try {
formattedCode = JSON.stringify(JSON.parse(code), null, 2);
} catch (e: unknown) {
formattedCode = code;
}
}
-
- blocks.set(id, JSON.stringify({
- code: formattedCode,
- language: currentLanguage,
- raw: true
- }));
- processedLines.push('');
+
+ blocks.set(
+ id,
+ JSON.stringify({
+ code: formattedCode,
+ language: currentLanguage,
+ raw: true,
+ }),
+ );
+ processedLines.push("");
processedLines.push(id);
- processedLines.push('');
+ processedLines.push("");
}
return {
- text: processedLines.join('\n'),
- blocks
+ text: processedLines.join("\n"),
+ blocks,
};
}
@@ -312,22 +345,22 @@ function processCodeBlocks(text: string): { text: string; blocks: Map): string {
let result = text;
-
+
for (const [id, blockData] of blocks) {
try {
const { code, language } = JSON.parse(blockData);
-
+
let html;
if (language && hljs.getLanguage(language)) {
try {
const highlighted = hljs.highlight(code, {
language,
- ignoreIllegals: true
+ ignoreIllegals: true,
}).value;
html = `
${highlighted}
`;
} catch (e: unknown) {
- console.warn('Failed to highlight code block:', e);
- html = `
${code}
`;
+ console.warn("Failed to highlight code block:", e);
+ html = `
`;
- }
+ },
);
}
@@ -95,20 +114,24 @@ function fixStemBlocks(html: string): string {
* Post-processes asciidoctor HTML output to add wikilink and nostr address rendering.
* This function should be called after asciidoctor.convert() to enhance the HTML output.
*/
-export async function postProcessAsciidoctorHtml(html: string): Promise {
+export async function postProcessAsciidoctorHtml(
+ html: string,
+): Promise {
if (!html) return html;
-
+
try {
- // First process wikilinks
- let processedHtml = replaceWikilinks(html);
-
+ console.log('HTML before replaceWikilinks:', html);
+ // First process AsciiDoctor-generated anchors
+ let processedHtml = replaceAsciiDocAnchors(html);
+ // Then process wikilinks in [[...]] format (if any remain)
+ processedHtml = replaceWikilinks(processedHtml);
// Then process nostr addresses (but not those already in links)
processedHtml = await processNostrAddresses(processedHtml);
processedHtml = fixStemBlocks(processedHtml); // Fix math blocks for MathJax
-
+
return processedHtml;
} catch (error) {
- console.error('Error in postProcessAsciidoctorHtml:', error);
+ console.error("Error in postProcessAsciidoctorHtml:", error);
return html; // Return original HTML if processing fails
}
-}
\ No newline at end of file
+}
diff --git a/src/lib/utils/markup/basicMarkupParser.ts b/src/lib/utils/markup/basicMarkupParser.ts
index a79833b..f829462 100644
--- a/src/lib/utils/markup/basicMarkupParser.ts
+++ b/src/lib/utils/markup/basicMarkupParser.ts
@@ -1,6 +1,6 @@
-import { processNostrIdentifiers } from '../nostrUtils';
-import * as emoji from 'node-emoji';
-import { nip19 } from 'nostr-tools';
+import { processNostrIdentifiers } from "../nostrUtils";
+import * as emoji from "node-emoji";
+import { nip19 } from "nostr-tools";
/* Regex constants for basic markup parsing */
@@ -23,37 +23,42 @@ const DIRECT_LINK = /(?"]+)(?!["'])/g;
const IMAGE_EXTENSIONS = /\.(jpg|jpeg|gif|png|webp|svg)$/i;
const VIDEO_URL_REGEX = /https?:\/\/[^\s<]+\.(?:mp4|webm|mov|avi)(?:[^\s<]*)?/i;
const AUDIO_URL_REGEX = /https?:\/\/[^\s<]+\.(?:mp3|wav|ogg|m4a)(?:[^\s<]*)?/i;
-const YOUTUBE_URL_REGEX = /https?:\/\/(?:www\.)?(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/|youtube-nocookie\.com\/embed\/)([a-zA-Z0-9_-]{11})(?:[^\s<]*)?/i;
+const YOUTUBE_URL_REGEX =
+ /https?:\/\/(?:www\.)?(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/|youtube-nocookie\.com\/embed\/)([a-zA-Z0-9_-]{11})(?:[^\s<]*)?/i;
// Add this helper function near the top:
function replaceAlexandriaNostrLinks(text: string): string {
// Regex for Alexandria/localhost URLs
- const alexandriaPattern = /^https?:\/\/((next-)?alexandria\.gitcitadel\.(eu|com)|localhost(:\d+)?)/i;
+ const alexandriaPattern =
+ /^https?:\/\/((next-)?alexandria\.gitcitadel\.(eu|com)|localhost(:\d+)?)/i;
// Regex for bech32 Nostr identifiers
const bech32Pattern = /(npub|nprofile|note|nevent|naddr)[a-zA-Z0-9]{20,}/;
// Regex for 64-char hex
const hexPattern = /\b[a-fA-F0-9]{64}\b/;
// 1. Alexandria/localhost markup links
- text = text.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, (match, _label, url) => {
- if (alexandriaPattern.test(url)) {
- if (/[?&]d=/.test(url)) return match;
- const hexMatch = url.match(hexPattern);
- if (hexMatch) {
- try {
- const nevent = nip19.neventEncode({ id: hexMatch[0] });
- return `nostr:${nevent}`;
- } catch {
- return match;
+ text = text.replace(
+ /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g,
+ (match, _label, url) => {
+ if (alexandriaPattern.test(url)) {
+ if (/[?&]d=/.test(url)) return match;
+ const hexMatch = url.match(hexPattern);
+ if (hexMatch) {
+ try {
+ const nevent = nip19.neventEncode({ id: hexMatch[0] });
+ return `nostr:${nevent}`;
+ } catch {
+ return match;
+ }
+ }
+ const bech32Match = url.match(bech32Pattern);
+ if (bech32Match) {
+ return `nostr:${bech32Match[0]}`;
}
}
- const bech32Match = url.match(bech32Pattern);
- if (bech32Match) {
- return `nostr:${bech32Match[0]}`;
- }
- }
- return match;
- });
+ return match;
+ },
+ );
// 2. Alexandria/localhost bare URLs and non-Alexandria/localhost URLs with Nostr identifiers
text = text.replace(/https?:\/\/[^\s)\]]+/g, (url) => {
@@ -96,12 +101,18 @@ function replaceAlexandriaNostrLinks(text: string): string {
// Utility to strip tracking parameters from URLs
function stripTrackingParams(url: string): string {
// List of tracking params to remove
- const trackingParams = [/^utm_/i, /^fbclid$/i, /^gclid$/i, /^tracking$/i, /^ref$/i];
+ const trackingParams = [
+ /^utm_/i,
+ /^fbclid$/i,
+ /^gclid$/i,
+ /^tracking$/i,
+ /^ref$/i,
+ ];
try {
// Absolute URL
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)) {
const parsed = new URL(url);
- trackingParams.forEach(pattern => {
+ trackingParams.forEach((pattern) => {
for (const key of Array.from(parsed.searchParams.keys())) {
if (pattern.test(key)) {
parsed.searchParams.delete(key);
@@ -109,19 +120,24 @@ function stripTrackingParams(url: string): string {
}
});
const queryString = parsed.searchParams.toString();
- return parsed.origin + parsed.pathname + (queryString ? '?' + queryString : '') + (parsed.hash || '');
+ return (
+ parsed.origin +
+ parsed.pathname +
+ (queryString ? "?" + queryString : "") +
+ (parsed.hash || "")
+ );
} else {
// Relative URL: parse query string manually
- const [path, queryAndHash = ''] = url.split('?');
- const [query = '', hash = ''] = queryAndHash.split('#');
+ const [path, queryAndHash = ""] = url.split("?");
+ const [query = "", hash = ""] = queryAndHash.split("#");
if (!query) return url;
- const params = query.split('&').filter(Boolean);
- const filtered = params.filter(param => {
- const [key] = param.split('=');
- return !trackingParams.some(pattern => pattern.test(key));
+ const params = query.split("&").filter(Boolean);
+ const filtered = params.filter((param) => {
+ const [key] = param.split("=");
+ return !trackingParams.some((pattern) => pattern.test(key));
});
- const queryString = filtered.length ? '?' + filtered.join('&') : '';
- const hashString = hash ? '#' + hash : '';
+ const queryString = filtered.length ? "?" + filtered.join("&") : "";
+ const hashString = hash ? "#" + hash : "";
return path + queryString + hashString;
}
} catch {
@@ -132,38 +148,45 @@ function stripTrackingParams(url: string): string {
function normalizeDTag(input: string): string {
return input
.toLowerCase()
- .replace(/[^\p{L}\p{N}]/gu, '-')
- .replace(/-+/g, '-')
- .replace(/^-|-$/g, '');
+ .replace(/[^\p{L}\p{N}]/gu, "-")
+ .replace(/-+/g, "-")
+ .replace(/^-|-$/g, "");
}
function replaceWikilinks(text: string): string {
// [[target page]] or [[target page|display text]]
- return text.replace(/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, (_match, target, label) => {
- const normalized = normalizeDTag(target.trim());
- const display = (label || target).trim();
- const url = `./events?d=${normalized}`;
- // Output as a clickable with the [[display]] format and matching link colors
- return `${display}`;
- });
+ return text.replace(
+ /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g,
+ (_match, target, label) => {
+ const normalized = normalizeDTag(target.trim());
+ const display = (label || target).trim();
+ const url = `./events?d=${normalized}`;
+ // Output as a clickable with the [[display]] format and matching link colors
+ return `${display}`;
+ },
+ );
}
-function renderListGroup(lines: string[], typeHint?: 'ol' | 'ul'): string {
- function parseList(start: number, indent: number, type: 'ol' | 'ul'): [string, number] {
- let html = '';
+function renderListGroup(lines: string[], typeHint?: "ol" | "ul"): string {
+ function parseList(
+ start: number,
+ indent: number,
+ type: "ol" | "ul",
+ ): [string, number] {
+ let html = "";
let i = start;
- html += `<${type} class="${type === 'ol' ? 'list-decimal' : 'list-disc'} ml-6 mb-2">`;
+ html += `<${type} class="${type === "ol" ? "list-decimal" : "list-disc"} ml-6 mb-2">`;
while (i < lines.length) {
const line = lines[i];
const match = line.match(/^([ \t]*)([*+-]|\d+\.)[ \t]+(.*)$/);
if (!match) break;
- const lineIndent = match[1].replace(/\t/g, ' ').length;
+ const lineIndent = match[1].replace(/\t/g, " ").length;
const isOrdered = /\d+\./.test(match[2]);
- const itemType = isOrdered ? 'ol' : 'ul';
+ const itemType = isOrdered ? "ol" : "ul";
if (lineIndent > indent) {
// Nested list
const [nestedHtml, consumed] = parseList(i, lineIndent, itemType);
- html = html.replace(/<\/li>$/, '') + nestedHtml + '';
+ html = html.replace(/<\/li>$/, "") + nestedHtml + "";
i = consumed;
continue;
}
@@ -175,35 +198,39 @@ function renderListGroup(lines: string[], typeHint?: 'ol' | 'ul'): string {
if (i + 1 < lines.length) {
const nextMatch = lines[i + 1].match(/^([ \t]*)([*+-]|\d+\.)[ \t]+/);
if (nextMatch) {
- const nextIndent = nextMatch[1].replace(/\t/g, ' ').length;
- const nextType = /\d+\./.test(nextMatch[2]) ? 'ol' : 'ul';
+ const nextIndent = nextMatch[1].replace(/\t/g, " ").length;
+ const nextType = /\d+\./.test(nextMatch[2]) ? "ol" : "ul";
if (nextIndent > lineIndent) {
- const [nestedHtml, consumed] = parseList(i + 1, nextIndent, nextType);
+ const [nestedHtml, consumed] = parseList(
+ i + 1,
+ nextIndent,
+ nextType,
+ );
html += nestedHtml;
i = consumed - 1;
}
}
}
- html += '';
+ html += "";
i++;
}
html += `${type}>`;
return [html, i];
}
- if (!lines.length) return '';
+ if (!lines.length) return "";
const firstLine = lines[0];
const match = firstLine.match(/^([ \t]*)([*+-]|\d+\.)[ \t]+/);
- const indent = match ? match[1].replace(/\t/g, ' ').length : 0;
- const type = typeHint || (match && /\d+\./.test(match[2]) ? 'ol' : 'ul');
+ const indent = match ? match[1].replace(/\t/g, " ").length : 0;
+ const type = typeHint || (match && /\d+\./.test(match[2]) ? "ol" : "ul");
const [html] = parseList(0, indent, type);
return html;
}
function processBasicFormatting(content: string): string {
- if (!content) return '';
-
+ if (!content) return "";
+
let processedText = content;
-
+
try {
// Sanitize Alexandria Nostr links before further processing
processedText = replaceAlexandriaNostrLinks(processedText);
@@ -214,17 +241,17 @@ function processBasicFormatting(content: string): string {
if (YOUTUBE_URL_REGEX.test(url)) {
const videoId = extractYouTubeVideoId(url);
if (videoId) {
- return ``;
+ return ``;
}
}
if (VIDEO_URL_REGEX.test(url)) {
- return ``;
+ return ``;
}
if (AUDIO_URL_REGEX.test(url)) {
- return ``;
+ return ``;
}
// Only render if the url ends with a direct image extension
- if (IMAGE_EXTENSIONS.test(url.split('?')[0])) {
+ if (IMAGE_EXTENSIONS.test(url.split("?")[0])) {
return ``;
}
// Otherwise, render as a clickable link
@@ -232,19 +259,21 @@ function processBasicFormatting(content: string): string {
});
// Process markup links
- processedText = processedText.replace(MARKUP_LINK, (match, text, url) =>
- `${text}`
+ processedText = processedText.replace(
+ MARKUP_LINK,
+ (match, text, url) =>
+ `${text}`,
);
// Process WebSocket URLs
- processedText = processedText.replace(WSS_URL, match => {
+ processedText = processedText.replace(WSS_URL, (match) => {
// Remove 'wss://' from the start and any trailing slashes
- const cleanUrl = match.slice(6).replace(/\/+$/, '');
+ const cleanUrl = match.slice(6).replace(/\/+$/, "");
return `${match}`;
});
// Process direct media URLs and auto-link all URLs
- processedText = processedText.replace(DIRECT_LINK, match => {
+ processedText = processedText.replace(DIRECT_LINK, (match) => {
const clean = stripTrackingParams(match);
if (YOUTUBE_URL_REGEX.test(clean)) {
const videoId = extractYouTubeVideoId(clean);
@@ -259,30 +288,36 @@ function processBasicFormatting(content: string): string {
return ``;
}
// Only render if the url ends with a direct image extension
- if (IMAGE_EXTENSIONS.test(clean.split('?')[0])) {
+ if (IMAGE_EXTENSIONS.test(clean.split("?")[0])) {
return ``;
}
// Otherwise, render as a clickable link
return `${clean}`;
});
-
+
// Process text formatting
- processedText = processedText.replace(BOLD_REGEX, '$2');
- processedText = processedText.replace(ITALIC_REGEX, match => {
- const text = match.replace(/^_+|_+$/g, '');
+ processedText = processedText.replace(BOLD_REGEX, "$2");
+ processedText = processedText.replace(ITALIC_REGEX, (match) => {
+ const text = match.replace(/^_+|_+$/g, "");
return `${text}`;
});
- processedText = processedText.replace(STRIKETHROUGH_REGEX, (match, doubleText, singleText) => {
- const text = doubleText || singleText;
- return `${text}`;
- });
+ processedText = processedText.replace(
+ STRIKETHROUGH_REGEX,
+ (match, doubleText, singleText) => {
+ const text = doubleText || singleText;
+ return `${text}`;
+ },
+ );
// Process hashtags
- processedText = processedText.replace(HASHTAG_REGEX, '#$1');
+ processedText = processedText.replace(
+ HASHTAG_REGEX,
+ '#$1',
+ );
// --- Improved List Grouping and Parsing ---
- const lines = processedText.split('\n');
- let output = '';
+ const lines = processedText.split("\n");
+ let output = "";
let buffer: string[] = [];
let inList = false;
for (let i = 0; i < lines.length; i++) {
@@ -294,23 +329,22 @@ function processBasicFormatting(content: string): string {
if (inList) {
const firstLine = buffer[0];
const isOrdered = /^\s*\d+\.\s+/.test(firstLine);
- output += renderListGroup(buffer, isOrdered ? 'ol' : 'ul');
+ output += renderListGroup(buffer, isOrdered ? "ol" : "ul");
buffer = [];
inList = false;
}
- output += (output && !output.endsWith('\n') ? '\n' : '') + line + '\n';
+ output += (output && !output.endsWith("\n") ? "\n" : "") + line + "\n";
}
}
if (buffer.length) {
const firstLine = buffer[0];
const isOrdered = /^\s*\d+\.\s+/.test(firstLine);
- output += renderListGroup(buffer, isOrdered ? 'ol' : 'ul');
+ output += renderListGroup(buffer, isOrdered ? "ol" : "ul");
}
processedText = output;
// --- End Improved List Grouping and Parsing ---
-
} catch (e: unknown) {
- console.error('Error in processBasicFormatting:', e);
+ console.error("Error in processBasicFormatting:", e);
}
return processedText;
@@ -318,61 +352,72 @@ function processBasicFormatting(content: string): string {
// Helper function to extract YouTube video ID
function extractYouTubeVideoId(url: string): string | null {
- const match = url.match(/(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/|youtube-nocookie\.com\/embed\/)([a-zA-Z0-9_-]{11})/);
+ const match = url.match(
+ /(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/|youtube-nocookie\.com\/embed\/)([a-zA-Z0-9_-]{11})/,
+ );
return match ? match[1] : null;
}
function processBlockquotes(content: string): string {
try {
- if (!content) return '';
-
- return content.replace(BLOCKQUOTE_REGEX, match => {
- const lines = match.split('\n').map(line => {
- return line.replace(/^[ \t]*>[ \t]?/, '').trim();
+ if (!content) return "";
+
+ return content.replace(BLOCKQUOTE_REGEX, (match) => {
+ const lines = match.split("\n").map((line) => {
+ return line.replace(/^[ \t]*>[ \t]?/, "").trim();
});
-
- return `
${
- lines.join('\n')
- }
`;
+
+ return `
${lines.join(
+ "\n",
+ )}
`;
});
} catch (e: unknown) {
- console.error('Error in processBlockquotes:', e);
+ console.error("Error in processBlockquotes:", e);
return content;
}
}
function processEmojiShortcuts(content: string): string {
try {
- return emoji.emojify(content, { fallback: (name: string) => {
- const emojiChar = emoji.get(name);
- return emojiChar || `:${name}:`;
- }});
+ return emoji.emojify(content, {
+ fallback: (name: string) => {
+ const emojiChar = emoji.get(name);
+ return emojiChar || `:${name}:`;
+ },
+ });
} catch (e: unknown) {
- console.error('Error in processEmojiShortcuts:', e);
+ console.error("Error in processEmojiShortcuts:", e);
return content;
}
}
export async function parseBasicmarkup(text: string): Promise {
- if (!text) return '';
-
+ if (!text) return "";
+
try {
// Process basic text formatting first
let processedText = processBasicFormatting(text);
// Process emoji shortcuts
processedText = processEmojiShortcuts(processedText);
-
+
// Process blockquotes
processedText = processBlockquotes(processedText);
-
+
// Process paragraphs - split by double newlines and wrap in p tags
+ // Skip wrapping if content already contains block-level elements
processedText = processedText
.split(/\n\n+/)
- .map(para => para.trim())
- .filter(para => para.length > 0)
- .map(para => `
${para}
`)
- .join('\n');
+ .map((para) => para.trim())
+ .filter((para) => para.length > 0)
+ .map((para) => {
+ // Skip wrapping if para already contains block-level elements
+ if (/<(div|h[1-6]|blockquote|table|pre|ul|ol|hr)/i.test(para)) {
+ return para;
+ }
+ return `
${para}
`;
+ })
+ .join("\n");
// Process Nostr identifiers last
processedText = await processNostrIdentifiers(processedText);
@@ -382,7 +427,7 @@ export async function parseBasicmarkup(text: string): Promise {
return processedText;
} catch (e: unknown) {
- console.error('Error in parseBasicmarkup:', e);
- return `
Error processing markup: ${(e as Error)?.message ?? 'Unknown error'}
`;
+ console.error("Error in parseBasicmarkup:", e);
+ return `
Error processing markup: ${(e as Error)?.message ?? "Unknown error"}
`;
}
-}
\ No newline at end of file
+}
diff --git a/src/lib/utils/markup/tikzRenderer.ts b/src/lib/utils/markup/tikzRenderer.ts
index 68c7e91..3e194b6 100644
--- a/src/lib/utils/markup/tikzRenderer.ts
+++ b/src/lib/utils/markup/tikzRenderer.ts
@@ -10,13 +10,13 @@ export function renderTikZ(tikzCode: string): string {
try {
// For now, we'll create a simple SVG placeholder
// In a full implementation, this would use node-tikzjax or similar library
-
+
// Extract TikZ content and create a basic SVG
const svgContent = createBasicSVG(tikzCode);
-
+
return svgContent;
} catch (error) {
- console.error('Failed to render TikZ:', error);
+ console.error("Failed to render TikZ:", error);
return `
TikZ Rendering Error
Failed to render TikZ diagram. Original code:
@@ -33,7 +33,7 @@ function createBasicSVG(tikzCode: string): string {
// Create a simple SVG with the TikZ code as text
const width = 400;
const height = 300;
-
+
return `
@@ -54,7 +54,7 @@ function createBasicSVG(tikzCode: string): string {
* Escapes HTML characters for safe display
*/
function escapeHtml(text: string): string {
- const div = document.createElement('div');
+ const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
-}
\ No newline at end of file
+}
diff --git a/src/lib/utils/mime.ts b/src/lib/utils/mime.ts
index 28f744e..123b46e 100644
--- a/src/lib/utils/mime.ts
+++ b/src/lib/utils/mime.ts
@@ -6,22 +6,24 @@
* - Addressable: 30000-39999 (latest per d-tag stored)
* - Regular: all other kinds (stored by relays)
*/
-export function getEventType(kind: number): 'regular' | 'replaceable' | 'ephemeral' | 'addressable' {
+export function getEventType(
+ kind: number,
+): "regular" | "replaceable" | "ephemeral" | "addressable" {
// Check special ranges first
if (kind >= 30000 && kind < 40000) {
- return 'addressable';
+ return "addressable";
}
-
+
if (kind >= 20000 && kind < 30000) {
- return 'ephemeral';
+ return "ephemeral";
}
-
+
if ((kind >= 10000 && kind < 20000) || kind === 0 || kind === 3) {
- return 'replaceable';
+ return "replaceable";
}
-
+
// Everything else is regular
- return 'regular';
+ return "regular";
}
/**
@@ -36,9 +38,10 @@ export function getMimeTags(kind: number): [string, string][] {
// Determine replaceability based on event type
const eventType = getEventType(kind);
- const replaceability = (eventType === 'replaceable' || eventType === 'addressable')
- ? "replaceable"
- : "nonreplaceable";
+ const replaceability =
+ eventType === "replaceable" || eventType === "addressable"
+ ? "replaceable"
+ : "nonreplaceable";
switch (kind) {
// Short text note
@@ -93,4 +96,4 @@ export function getMimeTags(kind: number): [string, string][] {
}
return [mTag, MTag];
-}
\ No newline at end of file
+}
diff --git a/src/lib/utils/nostrUtils.ts b/src/lib/utils/nostrUtils.ts
index ff7440b..739c8f5 100644
--- a/src/lib/utils/nostrUtils.ts
+++ b/src/lib/utils/nostrUtils.ts
@@ -1,22 +1,26 @@
-import { get } from 'svelte/store';
-import { nip19 } from 'nostr-tools';
-import { ndkInstance } from '$lib/ndk';
-import { npubCache } from './npubCache';
+import { get } from "svelte/store";
+import { nip19 } from "nostr-tools";
+import { ndkInstance } from "$lib/ndk";
+import { npubCache } from "./npubCache";
import NDK, { NDKEvent, NDKRelaySet, NDKUser } from "@nostr-dev-kit/ndk";
import type { NDKFilter, NDKKind } from "@nostr-dev-kit/ndk";
import { standardRelays, fallbackRelays, anonymousRelays } from "$lib/consts";
-import { NDKRelaySet as NDKRelaySetFromNDK } from '@nostr-dev-kit/ndk';
-import { sha256 } from '@noble/hashes/sha256';
-import { schnorr } from '@noble/curves/secp256k1';
-import { bytesToHex } from '@noble/hashes/utils';
+import { NDKRelaySet as NDKRelaySetFromNDK } from "@nostr-dev-kit/ndk";
+import { sha256 } from "@noble/hashes/sha256";
+import { schnorr } from "@noble/curves/secp256k1";
+import { bytesToHex } from "@noble/hashes/utils";
-const badgeCheckSvg = ''
+const badgeCheckSvg =
+ '';
-const graduationCapSvg = '';
+const graduationCapSvg =
+ '';
// Regular expressions for Nostr identifiers - match the entire identifier including any prefix
-export const NOSTR_PROFILE_REGEX = /(?': '>',
- '"': '"',
- "'": '''
+ "&": "&",
+ "<": "<",
+ ">": ">",
+ '"': """,
+ "'": "'",
};
- return text.replace(/[&<>"']/g, char => htmlEscapes[char]);
+ return text.replace(/[&<>"']/g, (char) => htmlEscapes[char]);
}
/**
* Get user metadata for a nostr identifier (npub or nprofile)
*/
-export async function getUserMetadata(identifier: string): Promise {
+export async function getUserMetadata(
+ identifier: string,
+): Promise {
// Remove nostr: prefix if present
- const cleanId = identifier.replace(/^nostr:/, '');
-
+ const cleanId = identifier.replace(/^nostr:/, "");
+
if (npubCache.has(cleanId)) {
return npubCache.get(cleanId)!;
}
@@ -71,17 +77,23 @@ export async function getUserMetadata(identifier: string): Promise
// Handle different identifier types
let pubkey: string;
- if (decoded.type === 'npub') {
+ if (decoded.type === "npub") {
pubkey = decoded.data;
- } else if (decoded.type === 'nprofile') {
+ } else if (decoded.type === "nprofile") {
pubkey = decoded.data.pubkey;
} else {
npubCache.set(cleanId, fallback);
return fallback;
}
- const profileEvent = await fetchEventWithFallback(ndk, { kinds: [0], authors: [pubkey] });
- const profile = profileEvent && profileEvent.content ? JSON.parse(profileEvent.content) : null;
+ const profileEvent = await fetchEventWithFallback(ndk, {
+ kinds: [0],
+ authors: [pubkey],
+ });
+ const profile =
+ profileEvent && profileEvent.content
+ ? JSON.parse(profileEvent.content)
+ : null;
const metadata: NostrProfile = {
name: profile?.name || fallback.name,
@@ -91,9 +103,9 @@ export async function getUserMetadata(identifier: string): Promise
about: profile?.about,
banner: profile?.banner,
website: profile?.website,
- lud16: profile?.lud16
+ lud16: profile?.lud16,
};
-
+
npubCache.set(cleanId, metadata);
return metadata;
} catch (e) {
@@ -105,27 +117,33 @@ export async function getUserMetadata(identifier: string): Promise
/**
* Create a profile link element
*/
-export function createProfileLink(identifier: string, displayText: string | undefined): string {
- const cleanId = identifier.replace(/^nostr:/, '');
+export function createProfileLink(
+ identifier: string,
+ displayText: string | undefined,
+): string {
+ const cleanId = identifier.replace(/^nostr:/, "");
const escapedId = escapeHtml(cleanId);
const defaultText = `${cleanId.slice(0, 8)}...${cleanId.slice(-4)}`;
const escapedText = escapeHtml(displayText || defaultText);
-
+
return `@${escapedText}`;
}
/**
* Create a profile link element with a NIP-05 verification indicator.
*/
-export async function createProfileLinkWithVerification(identifier: string, displayText: string | undefined): Promise {
+export async function createProfileLinkWithVerification(
+ identifier: string,
+ displayText: string | undefined,
+): Promise {
const ndk = get(ndkInstance) as NDK;
if (!ndk) {
return createProfileLink(identifier, displayText);
}
- const cleanId = identifier.replace(/^nostr:/, '');
+ const cleanId = identifier.replace(/^nostr:/, "");
const escapedId = escapeHtml(cleanId);
- const isNpub = cleanId.startsWith('npub');
+ const isNpub = cleanId.startsWith("npub");
let user: NDKUser;
if (isNpub) {
@@ -134,19 +152,23 @@ export async function createProfileLinkWithVerification(identifier: string, disp
user = ndk.getUser({ pubkey: cleanId });
}
- const userRelays = Array.from(ndk.pool?.relays.values() || []).map(r => r.url);
+ const userRelays = Array.from(ndk.pool?.relays.values() || []).map(
+ (r) => r.url,
+ );
const allRelays = [
...standardRelays,
...userRelays,
- ...fallbackRelays
+ ...fallbackRelays,
].filter((url, idx, arr) => arr.indexOf(url) === idx);
const relaySet = NDKRelaySetFromNDK.fromRelayUrls(allRelays, ndk);
const profileEvent = await ndk.fetchEvent(
{ kinds: [0], authors: [user.pubkey] },
undefined,
- relaySet
+ relaySet,
);
- const profile = profileEvent?.content ? JSON.parse(profileEvent.content) : null;
+ const profile = profileEvent?.content
+ ? JSON.parse(profileEvent.content)
+ : null;
const nip05 = profile?.nip05;
if (!nip05) {
@@ -155,20 +177,24 @@ export async function createProfileLinkWithVerification(identifier: string, disp
const defaultText = `${cleanId.slice(0, 8)}...${cleanId.slice(-4)}`;
const escapedText = escapeHtml(displayText || defaultText);
- const displayIdentifier = profile?.displayName ?? profile?.display_name ?? profile?.name ?? escapedText;
+ const displayIdentifier =
+ profile?.displayName ??
+ profile?.display_name ??
+ profile?.name ??
+ escapedText;
const isVerified = await user.validateNip05(nip05);
-
+
if (!isVerified) {
return createProfileLink(identifier, displayText);
}
-
+
// TODO: Make this work with an enum in case we add more types.
- const type = nip05.endsWith('edu') ? 'edu' : 'standard';
+ const type = nip05.endsWith("edu") ? "edu" : "standard";
switch (type) {
- case 'edu':
+ case "edu":
return `@${displayIdentifier}${graduationCapSvg}`;
- case 'standard':
+ case "standard":
return `@${displayIdentifier}${badgeCheckSvg}`;
}
}
@@ -176,18 +202,20 @@ export async function createProfileLinkWithVerification(identifier: string, disp
* Create a note link element
*/
function createNoteLink(identifier: string): string {
- const cleanId = identifier.replace(/^nostr:/, '');
+ const cleanId = identifier.replace(/^nostr:/, "");
const shortId = `${cleanId.slice(0, 12)}...${cleanId.slice(-8)}`;
const escapedId = escapeHtml(cleanId);
const escapedText = escapeHtml(shortId);
-
+
return `${escapedText}`;
}
/**
* Process Nostr identifiers in text
*/
-export async function processNostrIdentifiers(content: string): Promise {
+export async function processNostrIdentifiers(
+ content: string,
+): Promise {
let processedContent = content;
// Helper to check if a match is part of a URL
@@ -206,8 +234,8 @@ export async function processNostrIdentifiers(content: string): Promise
continue; // skip if part of a URL
}
let identifier = fullMatch;
- if (!identifier.startsWith('nostr:')) {
- identifier = 'nostr:' + identifier;
+ if (!identifier.startsWith("nostr:")) {
+ identifier = "nostr:" + identifier;
}
const metadata = await getUserMetadata(identifier);
const displayText = metadata.displayName || metadata.name;
@@ -224,8 +252,8 @@ export async function processNostrIdentifiers(content: string): Promise
continue; // skip if part of a URL
}
let identifier = fullMatch;
- if (!identifier.startsWith('nostr:')) {
- identifier = 'nostr:' + identifier;
+ if (!identifier.startsWith("nostr:")) {
+ identifier = "nostr:" + identifier;
}
const link = createNoteLink(identifier);
processedContent = processedContent.replace(fullMatch, link);
@@ -238,17 +266,17 @@ export async function getNpubFromNip05(nip05: string): Promise {
try {
const ndk = get(ndkInstance);
if (!ndk) {
- console.error('NDK not initialized');
+ console.error("NDK not initialized");
return null;
}
-
+
const user = await ndk.getUser({ nip05 });
if (!user || !user.npub) {
return null;
}
return user.npub;
} catch (error) {
- console.error('Error getting npub from nip05:', error);
+ console.error("Error getting npub from nip05:", error);
return null;
}
}
@@ -258,7 +286,7 @@ export async function getNpubFromNip05(nip05: string): Promise {
* Can be used in two ways:
* 1. Method style: promise.withTimeout(5000)
* 2. Function style: withTimeout(promise, 5000)
- *
+ *
* @param thisOrPromise Either the promise to timeout (function style) or the 'this' context (method style)
* @param timeoutMsOrPromise Timeout duration in milliseconds (function style) or the promise (method style)
* @returns The promise result if completed before timeout, otherwise throws an error
@@ -266,28 +294,28 @@ export async function getNpubFromNip05(nip05: string): Promise {
*/
export function withTimeout(
thisOrPromise: Promise | number,
- timeoutMsOrPromise?: number | Promise
+ timeoutMsOrPromise?: number | Promise,
): Promise {
// Handle method-style call (promise.withTimeout(5000))
- if (typeof thisOrPromise === 'number') {
+ if (typeof thisOrPromise === "number") {
const timeoutMs = thisOrPromise;
const promise = timeoutMsOrPromise as Promise;
return Promise.race([
promise,
- new Promise((_, reject) =>
- setTimeout(() => reject(new Error('Timeout')), timeoutMs)
- )
+ new Promise((_, reject) =>
+ setTimeout(() => reject(new Error("Timeout")), timeoutMs),
+ ),
]);
}
-
+
// Handle function-style call (withTimeout(promise, 5000))
const promise = thisOrPromise;
const timeoutMs = timeoutMsOrPromise as number;
return Promise.race([
promise,
- new Promise((_, reject) =>
- setTimeout(() => reject(new Error('Timeout')), timeoutMs)
- )
+ new Promise((_, reject) =>
+ setTimeout(() => reject(new Error("Timeout")), timeoutMs),
+ ),
]);
}
@@ -298,7 +326,10 @@ declare global {
}
}
-Promise.prototype.withTimeout = function(this: Promise, timeoutMs: number): Promise {
+Promise.prototype.withTimeout = function (
+ this: Promise,
+ timeoutMs: number,
+): Promise {
return withTimeout(timeoutMs, this);
};
@@ -311,24 +342,24 @@ Promise.prototype.withTimeout = function(this: Promise, timeoutMs: number)
export async function fetchEventWithFallback(
ndk: NDK,
filterOrId: string | NDKFilter,
- timeoutMs: number = 3000
+ timeoutMs: number = 3000,
): Promise {
// Get user relays if logged in
- const userRelays = ndk.activeUser ?
- Array.from(ndk.pool?.relays.values() || [])
- .filter(r => r.status === 1) // Only use connected relays
- .map(r => r.url) :
- [];
-
+ const userRelays = ndk.activeUser
+ ? Array.from(ndk.pool?.relays.values() || [])
+ .filter((r) => r.status === 1) // Only use connected relays
+ .map((r) => r.url)
+ : [];
+
// Determine which relays to use based on user authentication status
const isSignedIn = ndk.signer && ndk.activeUser;
const primaryRelays = isSignedIn ? standardRelays : anonymousRelays;
-
+
// Create three relay sets in priority order
const relaySets = [
- NDKRelaySetFromNDK.fromRelayUrls(primaryRelays, ndk), // 1. Primary relays (auth or anonymous)
- NDKRelaySetFromNDK.fromRelayUrls(userRelays, ndk), // 2. User relays (if logged in)
- NDKRelaySetFromNDK.fromRelayUrls(fallbackRelays, ndk) // 3. fallback relays (last resort)
+ NDKRelaySetFromNDK.fromRelayUrls(primaryRelays, ndk), // 1. Primary relays (auth or anonymous)
+ NDKRelaySetFromNDK.fromRelayUrls(userRelays, ndk), // 2. User relays (if logged in)
+ NDKRelaySetFromNDK.fromRelayUrls(fallbackRelays, ndk), // 3. fallback relays (last resort)
];
try {
@@ -336,47 +367,75 @@ export async function fetchEventWithFallback(
const triedRelaySets: string[] = [];
// Helper function to try fetching from a relay set
- async function tryFetchFromRelaySet(relaySet: NDKRelaySetFromNDK, setName: string): Promise {
+ async function tryFetchFromRelaySet(
+ relaySet: NDKRelaySetFromNDK,
+ setName: string,
+ ): Promise {
if (relaySet.relays.size === 0) return null;
triedRelaySets.push(setName);
-
- if (typeof filterOrId === 'string' && /^[0-9a-f]{64}$/i.test(filterOrId)) {
- return await ndk.fetchEvent({ ids: [filterOrId] }, undefined, relaySet).withTimeout(timeoutMs);
+
+ if (
+ typeof filterOrId === "string" &&
+ /^[0-9a-f]{64}$/i.test(filterOrId)
+ ) {
+ return await ndk
+ .fetchEvent({ ids: [filterOrId] }, undefined, relaySet)
+ .withTimeout(timeoutMs);
} else {
- const filter = typeof filterOrId === 'string' ? { ids: [filterOrId] } : filterOrId;
- const results = await ndk.fetchEvents(filter, undefined, relaySet).withTimeout(timeoutMs);
- return results instanceof Set ? Array.from(results)[0] as NDKEvent : null;
+ const filter =
+ typeof filterOrId === "string" ? { ids: [filterOrId] } : filterOrId;
+ const results = await ndk
+ .fetchEvents(filter, undefined, relaySet)
+ .withTimeout(timeoutMs);
+ return results instanceof Set
+ ? (Array.from(results)[0] as NDKEvent)
+ : null;
}
}
// Try each relay set in order
for (const [index, relaySet] of relaySets.entries()) {
- const setName = index === 0 ? (isSignedIn ? 'standard relays' : 'anonymous relays') :
- index === 1 ? 'user relays' :
- 'fallback relays';
-
+ const setName =
+ index === 0
+ ? isSignedIn
+ ? "standard relays"
+ : "anonymous relays"
+ : index === 1
+ ? "user relays"
+ : "fallback relays";
+
found = await tryFetchFromRelaySet(relaySet, setName);
if (found) break;
}
if (!found) {
const timeoutSeconds = timeoutMs / 1000;
- const relayUrls = relaySets.map((set, i) => {
- const setName = i === 0 ? (isSignedIn ? 'standard relays' : 'anonymous relays') :
- i === 1 ? 'user relays' :
- 'fallback relays';
- const urls = Array.from(set.relays).map(r => r.url);
- return urls.length > 0 ? `${setName} (${urls.join(', ')})` : null;
- }).filter(Boolean).join(', then ');
-
- console.warn(`Event not found after ${timeoutSeconds}s timeout. Tried ${relayUrls}. Some relays may be offline or slow.`);
+ const relayUrls = relaySets
+ .map((set, i) => {
+ const setName =
+ i === 0
+ ? isSignedIn
+ ? "standard relays"
+ : "anonymous relays"
+ : i === 1
+ ? "user relays"
+ : "fallback relays";
+ const urls = Array.from(set.relays).map((r) => r.url);
+ return urls.length > 0 ? `${setName} (${urls.join(", ")})` : null;
+ })
+ .filter(Boolean)
+ .join(", then ");
+
+ console.warn(
+ `Event not found after ${timeoutSeconds}s timeout. Tried ${relayUrls}. Some relays may be offline or slow.`,
+ );
return null;
}
// Always wrap as NDKEvent
return found instanceof NDKEvent ? found : new NDKEvent(ndk, found);
} catch (err) {
- console.error('Error in fetchEventWithFallback:', err);
+ console.error("Error in fetchEventWithFallback:", err);
return null;
}
}
@@ -390,7 +449,7 @@ export function toNpub(pubkey: string | undefined): string | null {
if (/^[a-f0-9]{64}$/i.test(pubkey)) {
return nip19.npubEncode(pubkey);
}
- if (pubkey.startsWith('npub1')) return pubkey;
+ if (pubkey.startsWith("npub1")) return pubkey;
return null;
} catch {
return null;
@@ -432,7 +491,7 @@ export function getEventHash(event: {
event.created_at,
event.kind,
event.tags,
- event.content
+ event.content,
]);
return bytesToHex(sha256(serialized));
}
@@ -447,4 +506,4 @@ export async function signEvent(event: {
const id = getEventHash(event);
const sig = await schnorr.sign(id, event.pubkey);
return bytesToHex(sig);
-}
\ No newline at end of file
+}
diff --git a/src/lib/utils/npubCache.ts b/src/lib/utils/npubCache.ts
index c99f879..4fc4405 100644
--- a/src/lib/utils/npubCache.ts
+++ b/src/lib/utils/npubCache.ts
@@ -1,4 +1,4 @@
-import type { NostrProfile } from './nostrUtils';
+import type { NostrProfile } from "./nostrUtils";
export type NpubMetadata = NostrProfile;
@@ -48,4 +48,4 @@ class NpubCache {
}
}
-export const npubCache = new NpubCache();
\ No newline at end of file
+export const npubCache = new NpubCache();
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 89660e5..9cb3197 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -7,12 +7,13 @@
import { HammerSolid } from "flowbite-svelte-icons";
// Get standard metadata for OpenGraph tags
- let title = 'Library of Alexandria';
+ let title = "Library of Alexandria";
let currentUrl = $page.url.href;
-
+
// Get default image and summary for the Alexandria website
- let image = '/screenshots/old_books.jpg';
- let summary = 'Alexandria is a digital library, utilizing Nostr events for curated publications and wiki pages.';
+ let image = "/screenshots/old_books.jpg";
+ let summary =
+ "Alexandria is a digital library, utilizing Nostr events for curated publications and wiki pages.";
onMount(() => {
const rect = document.body.getBoundingClientRect();
@@ -23,24 +24,24 @@
{title}
-
-
+
+
-
-
-
+
+
+
-
-
+
+
-
-
-
+
+
+
-
-
+
+
diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts
index 915324a..4ec9145 100644
--- a/src/routes/+layout.ts
+++ b/src/routes/+layout.ts
@@ -1,15 +1,21 @@
-import { feedTypeStorageKey } from '$lib/consts';
-import { FeedType } from '$lib/consts';
-import { getPersistedLogin, initNdk, loginWithExtension, ndkInstance } from '$lib/ndk';
-import Pharos, { pharosInstance } from '$lib/parser';
-import { feedType } from '$lib/stores';
-import type { LayoutLoad } from './$types';
+import { feedTypeStorageKey } from "$lib/consts";
+import { FeedType } from "$lib/consts";
+import {
+ getPersistedLogin,
+ initNdk,
+ loginWithExtension,
+ ndkInstance,
+} from "$lib/ndk";
+import Pharos, { pharosInstance } from "$lib/parser";
+import { feedType } from "$lib/stores";
+import type { LayoutLoad } from "./$types";
export const ssr = false;
export const load: LayoutLoad = () => {
- const initialFeedType = localStorage.getItem(feedTypeStorageKey) as FeedType
- ?? FeedType.StandardRelays;
+ const initialFeedType =
+ (localStorage.getItem(feedTypeStorageKey) as FeedType) ??
+ FeedType.StandardRelays;
feedType.set(initialFeedType);
const ndk = initNdk();
@@ -26,7 +32,9 @@ export const load: LayoutLoad = () => {
loginWithExtension(pubkey);
}
} catch (e) {
- console.warn(`Failed to login with extension: ${e}\n\nContinuing with anonymous session.`);
+ console.warn(
+ `Failed to login with extension: ${e}\n\nContinuing with anonymous session.`,
+ );
}
const parser = new Pharos(ndk);
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 02277f7..dc8de28 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -1,10 +1,15 @@
-
-
-
-
- Pardon our dust! The publication view is currently using an experimental loader, and may be unstable.
-
+
+
+
+ Pardon our dust! The publication view is currently using an experimental
+ loader, and may be unstable.
+
-
-
- We are easiest to contact over our Nostr address {@render userBadge("npub1s3ht77dq4zqnya8vjun5jp3p44pr794ru36d0ltxu65chljw8xjqd975wz", "GitCitadel")}. Or, you can visit us on our homepage
- import { Heading, P, A, Button, Label, Textarea, Input, Modal } from 'flowbite-svelte';
- import { ndkSignedIn, ndkInstance } from '$lib/ndk';
- import { standardRelays } from '$lib/consts';
- import type NDK from '@nostr-dev-kit/ndk';
- import { NDKEvent, NDKRelaySet } from '@nostr-dev-kit/ndk';
+
-
- You can contact us on Nostr {@render userBadge("npub1s3ht77dq4zqnya8vjun5jp3p44pr794ru36d0ltxu65chljw8xjqd975wz", "GitCitadel")} or you can view submitted issues on the Alexandria repo page.
+ You can contact us on Nostr {@render userBadge(
+ "npub1s3ht77dq4zqnya8vjun5jp3p44pr794ru36d0ltxu65chljw8xjqd975wz",
+ "GitCitadel",
+ )} or you can view submitted issues on the Alexandria repo page.
-
- Submit an issue
-
+
+ Submit an issue
+
- If you are logged into the Alexandria web application (using the button at the top-right of the window), then you can use the form, below, to submit an issue, that will appear on our repo page.
+ If you are logged into the Alexandria web application (using the button at
+ the top-right of the window), then you can use the form, below, to submit
+ an issue, that will appear on our repo page.