Browse Source

Ran `deno fmt`

master
buttercat1791 6 months ago
parent
commit
c06f102e4a
  1. 6
      postcss.config.js
  2. 24
      src/app.css
  3. 14
      src/app.html
  4. 13
      src/lib/a/README.md
  5. 65
      src/lib/components/event_input/eventServices.ts
  6. 2
      src/lib/components/event_input/types.ts
  7. 21
      src/lib/components/event_input/validation.ts
  8. 62
      src/lib/data_structures/publication_tree.ts
  9. 10
      src/lib/nostr/event.ts
  10. 23
      src/lib/nostr/format.ts
  11. 21
      src/lib/nostr/nip05.ts
  12. 148
      src/lib/nostr/nip58.ts
  13. 10
      src/lib/nostr/types.ts
  14. 14
      src/lib/stores/techStore.ts
  15. 12
      src/lib/stores/themeStore.ts
  16. 7
      src/lib/stores/userStore.ts
  17. 4
      src/lib/styles/cva.ts
  18. 3
      src/lib/utils/asciidoc_metadata.ts
  19. 26
      src/lib/utils/cache_manager.ts
  20. 7
      src/lib/utils/event_input_utils.ts
  21. 5
      src/lib/utils/event_search.ts
  22. 14
      src/lib/utils/markup/advancedMarkupParser.ts
  23. 12
      src/lib/utils/markup/basicMarkupParser.ts
  24. 5
      src/lib/utils/markup/embeddedMarkupParser.ts
  25. 57
      src/lib/utils/markup/markupUtils.ts
  26. 6
      src/lib/utils/nostrUtils.ts
  27. 67
      src/lib/utils/npubCache.ts
  28. 37
      src/lib/utils/profile_search.ts
  29. 226
      src/lib/utils/subscription_search.ts
  30. 73
      src/lib/utils/user_lists.ts
  31. 1
      src/routes/publication/[type]/[identifier]/+layout.server.ts
  32. 3
      src/routes/publication/[type]/[identifier]/+page.ts
  33. 8
      src/styles/notifications.css
  34. 3
      src/styles/scrollbar.css
  35. 71
      tests/unit/mathProcessing.test.ts
  36. 8
      tests/unit/tagExpansion.test.ts

6
postcss.config.js

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
export default {
plugins: {
'@tailwindcss/postcss': {}
}
}
"@tailwindcss/postcss": {},
},
};

24
src/app.css

@ -226,7 +226,8 @@ @@ -226,7 +226,8 @@
div.note-leather,
p.note-leather,
section.note-leather {
@apply bg-primary-50 dark:bg-primary-1000 text-gray-900 dark:text-gray-100 p-2 rounded;
@apply bg-primary-50 dark:bg-primary-1000 text-gray-900 dark:text-gray-100
p-2 rounded;
}
.edit div.note-leather:hover:not(:has(.note-leather:hover)),
@ -278,7 +279,8 @@ @@ -278,7 +279,8 @@
}
div.modal-leather > div {
@apply bg-primary-50 dark:bg-primary-950 border-b-[1px] border-primary-100 dark:border-primary-600;
@apply bg-primary-50 dark:bg-primary-950 border-b-[1px] border-primary-100
dark:border-primary-600;
}
div.modal-leather > div > h1,
@ -292,7 +294,9 @@ @@ -292,7 +294,9 @@
}
div.modal-leather button {
@apply bg-primary-50 hover:bg-primary-50 dark:bg-primary-950 dark:hover:bg-primary-950 text-gray-900 hover:text-primary-600 dark:text-gray-100 dark:hover:text-primary-400;
@apply bg-primary-50 hover:bg-primary-50 dark:bg-primary-950
dark:hover:bg-primary-950 text-gray-900 hover:text-primary-600
dark:text-gray-100 dark:hover:text-primary-400;
}
/* Navbar */
@ -504,7 +508,9 @@ @@ -504,7 +508,9 @@
/* Tooltip */
.tooltip-leather {
@apply fixed p-4 rounded shadow-lg bg-primary-50 dark:bg-primary-1000 text-gray-900 dark:text-gray-100 border border-gray-200 dark:border-gray-700 transition-colors duration-200;
@apply fixed p-4 rounded shadow-lg bg-primary-50 dark:bg-primary-1000
text-gray-900 dark:text-gray-100 border border-gray-200
dark:border-gray-700 transition-colors duration-200;
max-width: 400px;
z-index: 1000;
}
@ -585,7 +591,8 @@ @@ -585,7 +591,8 @@
}
a {
@apply underline cursor-pointer hover:text-primary-600 dark:hover:text-primary-400;
@apply underline cursor-pointer hover:text-primary-600
dark:hover:text-primary-400;
}
.imageblock {
@ -728,14 +735,15 @@ @@ -728,14 +735,15 @@
input[type="tel"],
input[type="url"],
textarea {
@apply bg-primary-50 dark:bg-primary-1000 text-gray-900 dark:text-gray-100 border-s-4 border-primary-200 rounded shadow-none;
@apply bg-primary-50 dark:bg-primary-1000 text-gray-900 dark:text-gray-100
border-s-4 border-primary-200 rounded shadow-none;
@apply focus:border-primary-600 dark:focus:border-primary-400;
}
/* Table of Contents highlighting */
.toc-highlight {
@apply bg-primary-200 dark:bg-primary-700 border-s-4 border-primary-600 rounded
dark:border-primary-400 font-medium;
@apply bg-primary-200 dark:bg-primary-700 border-s-4 border-primary-600
rounded dark:border-primary-400 font-medium;
transition: all 0.2s ease-in-out;
}

14
src/app.html

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en" data-tech="off">
<head>
<meta charset="utf-8" />
@ -19,7 +19,9 @@ @@ -19,7 +19,9 @@
<script>
try {
const v = localStorage.getItem("alexandria/showTech");
document.documentElement.dataset.tech = v === "true" ? "on" : "off";
document.documentElement.dataset.tech = v === "true"
? "on"
: "off";
} catch (_) {
/* no-op */
}
@ -46,14 +48,18 @@ @@ -46,14 +48,18 @@
},
};
</script>
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<script
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"
></script>
<!-- highlight.js for code highlighting -->
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css"
/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"
></script>
%sveltekit.head%
</head>

13
src/lib/a/README.md

@ -1,10 +1,11 @@ @@ -1,10 +1,11 @@
# Component Library
This folder contains a component library.
The idea is to have project-scoped reusable components that centralize theming and style rules,
so that main pages and layouts focus on the functionalities.
This folder contains a component library. The idea is to have project-scoped
reusable components that centralize theming and style rules, so that main pages
and layouts focus on the functionalities.
All components are based on Flowbite Svelte components,
which are built on top of Tailwind CSS.
All components are based on Flowbite Svelte components, which are built on top
of Tailwind CSS.
Keeping all the styles in one place allows us to easily change the look and feel of the application by switching themes.
Keeping all the styles in one place allows us to easily change the look and feel
of the application by switching themes.

65
src/lib/components/event_input/eventServices.ts

@ -14,21 +14,30 @@ import { anonymousRelays } from "$lib/consts"; @@ -14,21 +14,30 @@ import { anonymousRelays } from "$lib/consts";
import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk";
import { removeMetadataFromContent } from "$lib/utils/asciidoc_metadata";
import { build30040EventSet } from "$lib/utils/event_input_utils";
import type { EventData, TagData, PublishResult, LoadEventResult } from "./types";
import type {
EventData,
LoadEventResult,
PublishResult,
TagData,
} from "./types";
/**
* Converts TagData array to NDK-compatible format
*/
function convertTagsToNDKFormat(tags: TagData[]): string[][] {
return tags
.filter(tag => tag.key.trim() !== "")
.map(tag => [tag.key, ...tag.values]);
.filter((tag) => tag.key.trim() !== "")
.map((tag) => [tag.key, ...tag.values]);
}
/**
* Publishes an event to relays
*/
export async function publishEvent(ndk: any, eventData: EventData, tags: TagData[]): Promise<PublishResult> {
export async function publishEvent(
ndk: any,
eventData: EventData,
tags: TagData[],
): Promise<PublishResult> {
if (!ndk) {
return { success: false, error: "NDK context not available" };
}
@ -42,7 +51,10 @@ export async function publishEvent(ndk: any, eventData: EventData, tags: TagData @@ -42,7 +51,10 @@ export async function publishEvent(ndk: any, eventData: EventData, tags: TagData
const pubkeyString = String(pubkey);
if (!/^[a-fA-F0-9]{64}$/.test(pubkeyString)) {
return { success: false, error: "Invalid public key: must be a 64-character hex string." };
return {
success: false,
error: "Invalid public key: must be a 64-character hex string.",
};
}
const baseEvent = { pubkey: pubkeyString, created_at: eventData.createdAt };
@ -59,14 +71,18 @@ export async function publishEvent(ndk: any, eventData: EventData, tags: TagData @@ -59,14 +71,18 @@ export async function publishEvent(ndk: any, eventData: EventData, tags: TagData
try {
// Get the current d and title values from the UI
const dTagValue = tags.find(tag => tag.key === "d")?.values[0] || "";
const titleTagValue = tags.find(tag => tag.key === "title")?.values[0] || "";
const dTagValue = tags.find((tag) => tag.key === "d")?.values[0] || "";
const titleTagValue = tags.find((tag) =>
tag.key === "title"
)?.values[0] || "";
// Convert multi-value tags to the format expected by build30040EventSet
// Filter out d and title tags since we'll add them manually
const compatibleTags: [string, string][] = tags
.filter(tag => tag.key.trim() !== "" && tag.key !== "d" && tag.key !== "title")
.map(tag => [tag.key, tag.values[0] || ""] as [string, string]);
.filter((tag) =>
tag.key.trim() !== "" && tag.key !== "d" && tag.key !== "title"
)
.map((tag) => [tag.key, tag.values[0] || ""] as [string, string]);
const { indexEvent, sectionEvents } = build30040EventSet(
eventData.content,
@ -76,7 +92,9 @@ export async function publishEvent(ndk: any, eventData: EventData, tags: TagData @@ -76,7 +92,9 @@ export async function publishEvent(ndk: any, eventData: EventData, tags: TagData
);
// Override the d and title tags with the UI values if they exist
const finalTags = indexEvent.tags.filter(tag => tag[0] !== "d" && tag[0] !== "title");
const finalTags = indexEvent.tags.filter((tag) =>
tag[0] !== "d" && tag[0] !== "title"
);
if (dTagValue) {
finalTags.push(["d", dTagValue]);
}
@ -97,7 +115,9 @@ export async function publishEvent(ndk: any, eventData: EventData, tags: TagData @@ -97,7 +115,9 @@ export async function publishEvent(ndk: any, eventData: EventData, tags: TagData
console.error("Error in build30040EventSet:", error);
return {
success: false,
error: `Failed to build 30040 event set: ${error instanceof Error ? error.message : "Unknown error"}`
error: `Failed to build 30040 event set: ${
error instanceof Error ? error.message : "Unknown error"
}`,
};
}
} else {
@ -181,7 +201,10 @@ export async function publishEvent(ndk: any, eventData: EventData, tags: TagData @@ -181,7 +201,10 @@ export async function publishEvent(ndk: any, eventData: EventData, tags: TagData
console.log("publishEvent: Publishing to relays:", relays);
console.log("publishEvent: Anonymous relays:", anonymousRelays);
console.log("publishEvent: Active outbox relays:", get(activeOutboxRelays));
console.log(
"publishEvent: Active outbox relays:",
get(activeOutboxRelays),
);
console.log("publishEvent: Active inbox relays:", get(activeInboxRelays));
let published = false;
@ -236,7 +259,9 @@ export async function publishEvent(ndk: any, eventData: EventData, tags: TagData @@ -236,7 +259,9 @@ export async function publishEvent(ndk: any, eventData: EventData, tags: TagData
console.error("Error signing/publishing event:", signError);
return {
success: false,
error: `Failed to sign event: ${signError instanceof Error ? signError.message : "Unknown error"}`
error: `Failed to sign event: ${
signError instanceof Error ? signError.message : "Unknown error"
}`,
};
}
}
@ -245,7 +270,7 @@ export async function publishEvent(ndk: any, eventData: EventData, tags: TagData @@ -245,7 +270,7 @@ export async function publishEvent(ndk: any, eventData: EventData, tags: TagData
return {
success: true,
eventId: lastEventId || undefined,
relays: relaysPublished
relays: relaysPublished,
};
} else {
return { success: false, error: "Failed to publish to any relay." };
@ -255,13 +280,19 @@ export async function publishEvent(ndk: any, eventData: EventData, tags: TagData @@ -255,13 +280,19 @@ export async function publishEvent(ndk: any, eventData: EventData, tags: TagData
/**
* Loads an event by its hex ID
*/
export async function loadEvent(ndk: any, eventId: string): Promise<LoadEventResult | null> {
export async function loadEvent(
ndk: any,
eventId: string,
): Promise<LoadEventResult | null> {
if (!ndk) {
throw new Error("NDK context not available");
}
console.log("loadEvent: Starting search for event ID:", eventId);
console.log("loadEvent: NDK pool relays:", Array.from(ndk.pool.relays.values()).map((r: any) => r.url));
console.log(
"loadEvent: NDK pool relays:",
Array.from(ndk.pool.relays.values()).map((r: any) => r.url),
);
console.log("loadEvent: Active inbox relays:", get(activeInboxRelays));
console.log("loadEvent: Active outbox relays:", get(activeOutboxRelays));
@ -279,7 +310,7 @@ export async function loadEvent(ndk: any, eventId: string): Promise<LoadEventRes @@ -279,7 +310,7 @@ export async function loadEvent(ndk: any, eventId: string): Promise<LoadEventRes
// Convert NDK tags format to our format
const tags: TagData[] = foundEvent.tags.map((tag: string[]) => ({
key: tag[0] || "",
values: tag.slice(1)
values: tag.slice(1),
}));
return { eventData, tags };

2
src/lib/components/event_input/types.ts

@ -32,7 +32,7 @@ export interface LoadEventResult { @@ -32,7 +32,7 @@ export interface LoadEventResult {
}
export interface EventPreview {
type: 'standard_event' | '30040_index_event' | 'error';
type: "standard_event" | "30040_index_event" | "error";
event?: {
id: string;
pubkey: string;

21
src/lib/components/event_input/validation.ts

@ -6,15 +6,18 @@ import { get } from "svelte/store"; @@ -6,15 +6,18 @@ import { get } from "svelte/store";
import { userStore } from "$lib/stores/userStore";
import type { EventData, TagData, ValidationResult } from "./types";
import {
validateNotAsciidoc,
validateAsciiDoc,
validate30040EventSet,
validateAsciiDoc,
validateNotAsciidoc,
} from "$lib/utils/event_input_utils";
/**
* Validates an event and its tags
*/
export function validateEvent(eventData: EventData, tags: TagData[]): ValidationResult {
export function validateEvent(
eventData: EventData,
tags: TagData[],
): ValidationResult {
const userState = get(userStore);
const pubkey = userState.pubkey;
@ -35,11 +38,13 @@ export function validateEvent(eventData: EventData, tags: TagData[]): Validation @@ -35,11 +38,13 @@ export function validateEvent(eventData: EventData, tags: TagData[]): Validation
if (eventData.kind === 30040) {
// Check for required tags
const versionTag = tags.find(t => t.key === "version");
const dTag = tags.find(t => t.key === "d");
const titleTag = tags.find(t => t.key === "title");
const versionTag = tags.find((t) => t.key === "version");
const dTag = tags.find((t) => t.key === "d");
const titleTag = tags.find((t) => t.key === "title");
if (!versionTag || !versionTag.values[0] || versionTag.values[0].trim() === "") {
if (
!versionTag || !versionTag.values[0] || versionTag.values[0].trim() === ""
) {
return { valid: false, reason: "30040 events require a 'version' tag." };
}
@ -86,5 +91,5 @@ export function isValidTagKey(key: string): boolean { @@ -86,5 +91,5 @@ export function isValidTagKey(key: string): boolean {
* Validates that a tag has at least one value
*/
export function isValidTag(tag: TagData): boolean {
return isValidTagKey(tag.key) && tag.values.some(v => v.trim().length > 0);
return isValidTagKey(tag.key) && tag.values.some((v) => v.trim().length > 0);
}

62
src/lib/data_structures/publication_tree.ts

@ -253,7 +253,10 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> { @@ -253,7 +253,10 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> {
// Clear all nodes except the root to force fresh loading
const rootAddress = this.#root.address;
this.#nodes.clear();
this.#nodes.set(rootAddress, new Lazy<PublicationTreeNode>(() => Promise.resolve(this.#root)));
this.#nodes.set(
rootAddress,
new Lazy<PublicationTreeNode>(() => Promise.resolve(this.#root)),
);
// Clear events cache to ensure fresh data
this.#events.clear();
this.#eventCache.clear();
@ -501,7 +504,9 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> { @@ -501,7 +504,9 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> {
// AI-NOTE: Check if this node has already been visited
if (this.#visitedNodes.has(address)) {
console.debug(`[PublicationTree] Skipping already visited node: ${address}`);
console.debug(
`[PublicationTree] Skipping already visited node: ${address}`,
);
return { done: false, value: null };
}
@ -762,7 +767,9 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> { @@ -762,7 +767,9 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> {
#addNode(address: string, parentNode: PublicationTreeNode) {
// AI-NOTE: Add debugging to track node addition
console.debug(`[PublicationTree] Adding node ${address} to parent ${parentNode.address}`);
console.debug(
`[PublicationTree] Adding node ${address} to parent ${parentNode.address}`,
);
const lazyNode = new Lazy<PublicationTreeNode>(() =>
this.#resolveNode(address, parentNode)
@ -902,7 +909,11 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> { @@ -902,7 +909,11 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> {
this.#eventCache.set(address, fetchedEvent);
this.#events.set(address, fetchedEvent);
return await this.#buildNodeFromEvent(fetchedEvent, address, parentNode);
return await this.#buildNodeFromEvent(
fetchedEvent,
address,
parentNode,
);
}
} catch (error) {
console.debug(
@ -1017,7 +1028,9 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> { @@ -1017,7 +1028,9 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> {
// AI-NOTE: Remove e-tag processing from synchronous method
// E-tags should be resolved asynchronously in #resolveNode method
// Adding raw event IDs here causes duplicate processing
console.debug(`[PublicationTree] Found ${eTags.length} e-tags but skipping processing in buildNodeFromEvent`);
console.debug(
`[PublicationTree] Found ${eTags.length} e-tags but skipping processing in buildNodeFromEvent`,
);
}
const node: PublicationTreeNode = {
@ -1033,13 +1046,18 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> { @@ -1033,13 +1046,18 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> {
// Now directly adds child nodes to current node's children array
// Add children in the order they appear in the a-tags to preserve section order
// Use sequential processing to ensure order is maintained
console.log(`[PublicationTree] Adding ${childAddresses.length} children in order:`, childAddresses);
console.log(
`[PublicationTree] Adding ${childAddresses.length} children in order:`,
childAddresses,
);
for (const childAddress of childAddresses) {
console.log(`[PublicationTree] Adding child: ${childAddress}`);
try {
// Add the child node directly to the current node's children
this.#addNode(childAddress, node);
console.log(`[PublicationTree] Successfully added child: ${childAddress}`);
console.log(
`[PublicationTree] Successfully added child: ${childAddress}`,
);
} catch (error) {
console.warn(
`[PublicationTree] Error adding child ${childAddress} for ${node.address}:`,
@ -1061,23 +1079,43 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> { @@ -1061,23 +1079,43 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> {
// Check if this 30040 has any children (a-tags only, since e-tags are handled separately)
const hasChildren = event.tags.some((tag) => tag[0] === "a");
console.debug(`[PublicationTree] Node type for ${event.kind}:${event.pubkey}:${event.tags.find(t => t[0] === 'd')?.[1]} - hasChildren: ${hasChildren}, type: ${hasChildren ? 'Branch' : 'Leaf'}`);
console.debug(
`[PublicationTree] Node type for ${event.kind}:${event.pubkey}:${
event.tags.find((t) => t[0] === "d")?.[1]
} - hasChildren: ${hasChildren}, type: ${
hasChildren ? "Branch" : "Leaf"
}`,
);
return hasChildren ? PublicationTreeNodeType.Branch : PublicationTreeNodeType.Leaf;
return hasChildren
? PublicationTreeNodeType.Branch
: PublicationTreeNodeType.Leaf;
}
// Zettel kinds are always leaves
if ([30041, 30818, 30023].includes(event.kind)) {
console.debug(`[PublicationTree] Node type for ${event.kind}:${event.pubkey}:${event.tags.find(t => t[0] === 'd')?.[1]} - Zettel kind, type: Leaf`);
console.debug(
`[PublicationTree] Node type for ${event.kind}:${event.pubkey}:${
event.tags.find((t) => t[0] === "d")?.[1]
} - Zettel kind, type: Leaf`,
);
return PublicationTreeNodeType.Leaf;
}
// For other kinds, check if they have children (a-tags only)
const hasChildren = event.tags.some((tag) => tag[0] === "a");
console.debug(`[PublicationTree] Node type for ${event.kind}:${event.pubkey}:${event.tags.find(t => t[0] === 'd')?.[1]} - hasChildren: ${hasChildren}, type: ${hasChildren ? 'Branch' : 'Leaf'}`);
console.debug(
`[PublicationTree] Node type for ${event.kind}:${event.pubkey}:${
event.tags.find((t) => t[0] === "d")?.[1]
} - hasChildren: ${hasChildren}, type: ${
hasChildren ? "Branch" : "Leaf"
}`,
);
return hasChildren ? PublicationTreeNodeType.Branch : PublicationTreeNodeType.Leaf;
return hasChildren
? PublicationTreeNodeType.Branch
: PublicationTreeNodeType.Leaf;
}
// #endregion

10
src/lib/nostr/event.ts

@ -1 +1,9 @@ @@ -1 +1,9 @@
export type NostrEvent = { id:string; kind:number; pubkey:string; created_at:number; tags:string[][]; content:string; }; export type AddressPointer = string;
export type NostrEvent = {
id: string;
kind: number;
pubkey: string;
created_at: number;
tags: string[][];
content: string;
};
export type AddressPointer = string;

23
src/lib/nostr/format.ts

@ -1 +1,22 @@ @@ -1 +1,22 @@
export function shortenBech32(id:string, keepPrefix=true, head=8, tail=6){ if(!id) return ''; const i=id.indexOf('1'); const prefix=i>0? id.slice(0,i):''; const data=i>0? id.slice(i+1): id; const short = data.length>head+tail ? `${'${'}data.slice(0,head)}…${'${'}data.slice(-tail)}` : data; return keepPrefix && prefix ? `${'${'}prefix}1${'${'}short}` : short; } export function displayNameFrom(npub:string, p?:{ name?:string; display_name?:string; nip05?:string }){ return (p?.display_name?.trim() || p?.name?.trim() || (p?.nip05 && p.nip05.split('@')[0]) || shortenBech32(npub,true)); }
export function shortenBech32(
id: string,
keepPrefix = true,
head = 8,
tail = 6,
) {
if (!id) return "";
const i = id.indexOf("1");
const prefix = i > 0 ? id.slice(0, i) : "";
const data = i > 0 ? id.slice(i + 1) : id;
const short = data.length > head + tail
? `${"${"}data.slice(0,head)}…${"${"}data.slice(-tail)}`
: data;
return keepPrefix && prefix ? `${"${"}prefix}1${"${"}short}` : short;
}
export function displayNameFrom(
npub: string,
p?: { name?: string; display_name?: string; nip05?: string },
) {
return (p?.display_name?.trim() || p?.name?.trim() ||
(p?.nip05 && p.nip05.split("@")[0]) || shortenBech32(npub, true));
}

21
src/lib/nostr/nip05.ts

@ -1 +1,20 @@ @@ -1 +1,20 @@
export async function verifyNip05(nip05:string, pubkeyHex:string):Promise<boolean>{ try{ if(!nip05||!pubkeyHex) return false; const [name,domain]=nip05.toLowerCase().split('@'); if(!name||!domain) return false; const url=`https://${'${'}domain}/.well-known/nostr.json?name=${'${'}encodeURIComponent(name)}`; const res=await fetch(url,{ headers:{ Accept:'application/json' } }); if(!res.ok) return false; const json=await res.json(); const found=json?.names?.[name]; return typeof found==='string' && found.toLowerCase()===pubkeyHex.toLowerCase(); }catch{ return false; } }
export async function verifyNip05(
nip05: string,
pubkeyHex: string,
): Promise<boolean> {
try {
if (!nip05 || !pubkeyHex) return false;
const [name, domain] = nip05.toLowerCase().split("@");
if (!name || !domain) return false;
const url =
`https://${"${"}domain}/.well-known/nostr.json?name=${"${"}encodeURIComponent(name)}`;
const res = await fetch(url, { headers: { Accept: "application/json" } });
if (!res.ok) return false;
const json = await res.json();
const found = json?.names?.[name];
return typeof found === "string" &&
found.toLowerCase() === pubkeyHex.toLowerCase();
} catch {
return false;
}
}

148
src/lib/nostr/nip58.ts

@ -1 +1,147 @@ @@ -1 +1,147 @@
import type { NostrEvent, AddressPointer } from './event'; export type BadgeDefinition={ kind:30009; id:string; pubkey:string; d:string; a:AddressPointer; name?:string; description?:string; image?:{ url:string; size?:string }|null; thumbs:{ url:string; size?:string }[]; }; export type BadgeAward={ kind:8; id:string; pubkey:string; a:AddressPointer; recipients:{ pubkey:string; relay?:string }[]; }; export type ProfileBadges={ kind:30008; id:string; pubkey:string; pairs:{ a:AddressPointer; awardId:string; relay?:string }[]; }; export const isKind=(e:NostrEvent,k:number)=>e.kind===k; const val=(tags:string[][],name:string)=>tags.find(t=>t[0]===name)?.[1]; const vals=(tags:string[][],name:string)=>tags.filter(t=>t[0]===name).map(t=>t.slice(1)); export function parseBadgeDefinition(e:NostrEvent):BadgeDefinition|null{ if(e.kind!==30009) return null; const d=val(e.tags,'d'); if(!d) return null; const a:AddressPointer=`30009:${'${'}e.pubkey}:${'${'}d}`; const name=val(e.tags,'name')||undefined; const description=val(e.tags,'description')||undefined; const imageTag=vals(e.tags,'image')[0]; const image=imageTag? { url:imageTag[0], size:imageTag[1] }: null; const thumbs=vals(e.tags,'thumb').map(([url,size])=>({ url, size })); return { kind:30009, id:e.id, pubkey:e.pubkey, d, a, name, description, image, thumbs }; } export function parseBadgeAward(e:NostrEvent):BadgeAward|null{ if(e.kind!==8) return null; const atag=vals(e.tags,'a')[0]; if(!atag) return null; const a:AddressPointer=atag[0]; const recipients=vals(e.tags,'p').map(([pubkey,relay])=>({ pubkey, relay })); return { kind:8, id:e.id, pubkey:e.pubkey, a, recipients }; } export function parseProfileBadges(e:NostrEvent):ProfileBadges|null{ if(e.kind!==30008) return null; const d=val(e.tags,'d'); if(d!=='profile_badges') return null; const pairs: { a:AddressPointer; awardId:string; relay?:string }[]=[]; for(let i=0;i<e.tags.length;i++){ const t=e.tags[i]; if(t[0]==='a'){ const a=t[1]; const nxt=e.tags[i+1]; if(nxt && nxt[0]==='e'){ pairs.push({ a, awardId:nxt[1], relay:nxt[2] }); i++; } } } return { kind:30008, id:e.id, pubkey:e.pubkey, pairs }; } export type DisplayBadge={ def:BadgeDefinition; award:BadgeAward|null; issuer:string; thumbUrl:string|null; title:string; }; export function pickThumb(def:BadgeDefinition, prefer:( '16'|'32'|'64'|'256'|'512')[]=['32','64','256']):string|null{ for(const p of prefer){ const t=def.thumbs.find(t=>(t.size||'').startsWith(p+'x')); if(t) return t.url; } return def.image?.url || null; } export function buildDisplayBadgesForUser(userPubkey:string, defs:BadgeDefinition[], awards:BadgeAward[], profileBadges?:ProfileBadges|null, opts:{ issuerWhitelist?:Set<string>; max?:number }={}):DisplayBadge[]{ const byA=new Map<string,BadgeDefinition>(defs.map(d=>[d.a,d])); const byAwardId=new Map<string,BadgeAward>(awards.map(a=>[a.id,a])); const isWhitelisted=(issuer:string)=>!opts.issuerWhitelist || opts.issuerWhitelist.has(issuer); let out:DisplayBadge[]=[]; if(profileBadges && profileBadges.pubkey===userPubkey){ for(const {a,awardId} of profileBadges.pairs){ const def=byA.get(a); if(!def) continue; const award=byAwardId.get(awardId)||null; if(award && (award.a!==a || !award.recipients.find(r=>r.pubkey===userPubkey))) continue; if(!isWhitelisted(def.pubkey)) continue; out.push({ def, award, issuer:def.pubkey, thumbUrl: pickThumb(def), title: def.name || def.d }); } } else { for(const aw of awards){ if(!aw.recipients.find(r=>r.pubkey===userPubkey)) continue; const def=byA.get(aw.a); if(!def) continue; if(!isWhitelisted(def.pubkey)) continue; out.push({ def, award:aw, issuer:def.pubkey, thumbUrl: pickThumb(def), title: def.name || def.d }); } } if(opts.max && out.length>opts.max) out=out.slice(0,opts.max); return out; }
import type { AddressPointer, NostrEvent } from "./event";
export type BadgeDefinition = {
kind: 30009;
id: string;
pubkey: string;
d: string;
a: AddressPointer;
name?: string;
description?: string;
image?: { url: string; size?: string } | null;
thumbs: { url: string; size?: string }[];
};
export type BadgeAward = {
kind: 8;
id: string;
pubkey: string;
a: AddressPointer;
recipients: { pubkey: string; relay?: string }[];
};
export type ProfileBadges = {
kind: 30008;
id: string;
pubkey: string;
pairs: { a: AddressPointer; awardId: string; relay?: string }[];
};
export const isKind = (e: NostrEvent, k: number) => e.kind === k;
const val = (tags: string[][], name: string) =>
tags.find((t) => t[0] === name)?.[1];
const vals = (tags: string[][], name: string) =>
tags.filter((t) => t[0] === name).map((t) => t.slice(1));
export function parseBadgeDefinition(e: NostrEvent): BadgeDefinition | null {
if (e.kind !== 30009) return null;
const d = val(e.tags, "d");
if (!d) return null;
const a: AddressPointer = `30009:${"${"}e.pubkey}:${"${"}d}`;
const name = val(e.tags, "name") || undefined;
const description = val(e.tags, "description") || undefined;
const imageTag = vals(e.tags, "image")[0];
const image = imageTag ? { url: imageTag[0], size: imageTag[1] } : null;
const thumbs = vals(e.tags, "thumb").map(([url, size]) => ({ url, size }));
return {
kind: 30009,
id: e.id,
pubkey: e.pubkey,
d,
a,
name,
description,
image,
thumbs,
};
}
export function parseBadgeAward(e: NostrEvent): BadgeAward | null {
if (e.kind !== 8) return null;
const atag = vals(e.tags, "a")[0];
if (!atag) return null;
const a: AddressPointer = atag[0];
const recipients = vals(e.tags, "p").map(([pubkey, relay]) => ({
pubkey,
relay,
}));
return { kind: 8, id: e.id, pubkey: e.pubkey, a, recipients };
}
export function parseProfileBadges(e: NostrEvent): ProfileBadges | null {
if (e.kind !== 30008) return null;
const d = val(e.tags, "d");
if (d !== "profile_badges") return null;
const pairs: { a: AddressPointer; awardId: string; relay?: string }[] = [];
for (let i = 0; i < e.tags.length; i++) {
const t = e.tags[i];
if (t[0] === "a") {
const a = t[1];
const nxt = e.tags[i + 1];
if (nxt && nxt[0] === "e") {
pairs.push({ a, awardId: nxt[1], relay: nxt[2] });
i++;
}
}
}
return { kind: 30008, id: e.id, pubkey: e.pubkey, pairs };
}
export type DisplayBadge = {
def: BadgeDefinition;
award: BadgeAward | null;
issuer: string;
thumbUrl: string | null;
title: string;
};
export function pickThumb(
def: BadgeDefinition,
prefer: ("16" | "32" | "64" | "256" | "512")[] = ["32", "64", "256"],
): string | null {
for (const p of prefer) {
const t = def.thumbs.find((t) => (t.size || "").startsWith(p + "x"));
if (t) return t.url;
}
return def.image?.url || null;
}
export function buildDisplayBadgesForUser(
userPubkey: string,
defs: BadgeDefinition[],
awards: BadgeAward[],
profileBadges?: ProfileBadges | null,
opts: { issuerWhitelist?: Set<string>; max?: number } = {},
): DisplayBadge[] {
const byA = new Map<string, BadgeDefinition>(defs.map((d) => [d.a, d]));
const byAwardId = new Map<string, BadgeAward>(awards.map((a) => [a.id, a]));
const isWhitelisted = (issuer: string) =>
!opts.issuerWhitelist || opts.issuerWhitelist.has(issuer);
let out: DisplayBadge[] = [];
if (profileBadges && profileBadges.pubkey === userPubkey) {
for (const { a, awardId } of profileBadges.pairs) {
const def = byA.get(a);
if (!def) {
continue;
}
const award = byAwardId.get(awardId) || null;
if (
award &&
(award.a !== a ||
!award.recipients.find((r) => r.pubkey === userPubkey))
) continue;
if (!isWhitelisted(def.pubkey)) continue;
out.push({
def,
award,
issuer: def.pubkey,
thumbUrl: pickThumb(def),
title: def.name || def.d,
});
}
} else {for (const aw of awards) {
if (!aw.recipients.find((r) => r.pubkey === userPubkey)) continue;
const def = byA.get(aw.a);
if (!def) continue;
if (!isWhitelisted(def.pubkey)) continue;
out.push({
def,
award: aw,
issuer: def.pubkey,
thumbUrl: pickThumb(def),
title: def.name || def.d,
});
}}
if (opts.max && out.length > opts.max) out = out.slice(0, opts.max);
return out;
}

10
src/lib/nostr/types.ts

@ -1 +1,9 @@ @@ -1 +1,9 @@
export type NostrProfile = { name?:string; display_name?:string; picture?:string; about?:string; nip05?:string; lud16?:string; badges?: Array<{ label:string; color?:string }>; };
export type NostrProfile = {
name?: string;
display_name?: string;
picture?: string;
about?: string;
nip05?: string;
lud16?: string;
badges?: Array<{ label: string; color?: string }>;
};

14
src/lib/stores/techStore.ts

@ -1,16 +1,16 @@ @@ -1,16 +1,16 @@
import { writable } from 'svelte/store';
const KEY = 'alexandria/showTech';
import { writable } from "svelte/store";
const KEY = "alexandria/showTech";
// Default false unless explicitly set to 'true' in localStorage
const initial = typeof localStorage !== 'undefined'
? localStorage.getItem(KEY) === 'true'
const initial = typeof localStorage !== "undefined"
? localStorage.getItem(KEY) === "true"
: false;
export const showTech = writable<boolean>(initial);
showTech.subscribe(v => {
if (typeof document !== 'undefined') {
document.documentElement.dataset.tech = v ? 'on' : 'off';
showTech.subscribe((v) => {
if (typeof document !== "undefined") {
document.documentElement.dataset.tech = v ? "on" : "off";
localStorage.setItem(KEY, String(v));
}
});

12
src/lib/stores/themeStore.ts

@ -1,15 +1,15 @@ @@ -1,15 +1,15 @@
import { writable } from 'svelte/store';
import { writable } from "svelte/store";
const KEY = 'alexandria/theme';
const KEY = "alexandria/theme";
const initial =
(typeof localStorage !== 'undefined' && localStorage.getItem(KEY)) ||
'light';
(typeof localStorage !== "undefined" && localStorage.getItem(KEY)) ||
"light";
export const theme = writable(initial);
theme.subscribe(v => {
if (typeof document !== 'undefined') {
theme.subscribe((v) => {
if (typeof document !== "undefined") {
document.documentElement.dataset.theme = String(v);
localStorage.setItem(KEY, String(v));
}

7
src/lib/stores/userStore.ts

@ -16,7 +16,6 @@ import { @@ -16,7 +16,6 @@ import {
import { loginStorageKey } from "../consts.ts";
import { nip19 } from "nostr-tools";
export interface UserState {
pubkey: string | null;
npub: string | null;
@ -248,7 +247,11 @@ export async function loginWithExtension(ndk: NDK) { @@ -248,7 +247,11 @@ export async function loginWithExtension(ndk: NDK) {
/**
* Login with Amber (NIP-46)
*/
export async function loginWithAmber(amberSigner: NDKSigner, user: NDKUser, ndk: NDK) {
export async function loginWithAmber(
amberSigner: NDKSigner,
user: NDKUser,
ndk: NDK,
) {
if (!ndk) throw new Error("NDK not initialized");
// Only clear previous login state after successful login
const npub = user.npub;

4
src/lib/styles/cva.ts

@ -1,3 +1,3 @@ @@ -1,3 +1,3 @@
import { cva, type VariantProps } from 'class-variance-authority';
import { twMerge } from 'tailwind-merge';
import { cva, type VariantProps } from "class-variance-authority";
import { twMerge } from "tailwind-merge";
export { cva, twMerge, type VariantProps };

3
src/lib/utils/asciidoc_metadata.ts

@ -293,7 +293,8 @@ function stripSectionHeader(sectionContent: string): string { @@ -293,7 +293,8 @@ function stripSectionHeader(sectionContent: string): string {
!line.includes("<") &&
!line.match(/^:[^:]+:\s*.+$/) &&
line.trim() !== "" &&
!(line.match(/^[A-Za-z\s]+$/) && line.trim() !== "" && line.trim().split(/\s+/).length <= 2)
!(line.match(/^[A-Za-z\s]+$/) && line.trim() !== "" &&
line.trim().split(/\s+/).length <= 2)
) {
contentStart = i;
break;

26
src/lib/utils/cache_manager.ts

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
import { unifiedProfileCache } from './npubCache';
import { searchCache } from './searchCache';
import { indexEventCache } from './indexEventCache';
import { clearRelaySetCache } from '../ndk';
import { unifiedProfileCache } from "./npubCache";
import { searchCache } from "./searchCache";
import { indexEventCache } from "./indexEventCache";
import { clearRelaySetCache } from "../ndk";
/**
* Clears all application caches
@ -13,7 +13,7 @@ import { clearRelaySetCache } from '../ndk'; @@ -13,7 +13,7 @@ import { clearRelaySetCache } from '../ndk';
* - relaySetCache (relay configuration)
*/
export function clearAllCaches(): void {
console.log('[CacheManager] Clearing all application caches...');
console.log("[CacheManager] Clearing all application caches...");
// Clear in-memory caches
unifiedProfileCache.clear();
@ -24,7 +24,7 @@ export function clearAllCaches(): void { @@ -24,7 +24,7 @@ export function clearAllCaches(): void {
// Clear localStorage caches
clearLocalStorageCaches();
console.log('[CacheManager] All caches cleared successfully');
console.log("[CacheManager] All caches cleared successfully");
}
/**
@ -32,7 +32,7 @@ export function clearAllCaches(): void { @@ -32,7 +32,7 @@ export function clearAllCaches(): void {
* This is useful when profile pictures or metadata are stale
*/
export function clearProfileCaches(): void {
console.log('[CacheManager] Clearing profile-specific caches...');
console.log("[CacheManager] Clearing profile-specific caches...");
// Clear unified profile cache
unifiedProfileCache.clear();
@ -42,31 +42,33 @@ export function clearProfileCaches(): void { @@ -42,31 +42,33 @@ export function clearProfileCaches(): void {
// This is acceptable since profile searches are the most common
searchCache.clear();
console.log('[CacheManager] Profile caches cleared successfully');
console.log("[CacheManager] Profile caches cleared successfully");
}
/**
* Clears localStorage caches
*/
function clearLocalStorageCaches(): void {
if (typeof window === 'undefined') return;
if (typeof window === "undefined") return;
const keysToRemove: string[] = [];
// Find all localStorage keys that start with 'alexandria'
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith('alexandria')) {
if (key && key.startsWith("alexandria")) {
keysToRemove.push(key);
}
}
// Remove the keys
keysToRemove.forEach(key => {
keysToRemove.forEach((key) => {
localStorage.removeItem(key);
});
console.log(`[CacheManager] Cleared ${keysToRemove.length} localStorage items`);
console.log(
`[CacheManager] Cleared ${keysToRemove.length} localStorage items`,
);
}
/**

7
src/lib/utils/event_input_utils.ts

@ -204,7 +204,6 @@ function extractMarkdownTopHeader(content: string): string | null { @@ -204,7 +204,6 @@ function extractMarkdownTopHeader(content: string): string | null {
// Event Construction
// =========================
/**
* Builds a set of events for a 30040 publication: one 30040 index event and one 30041 event per section.
* Each 30041 gets a d-tag (normalized section header) and a title tag (raw section header).
@ -261,7 +260,8 @@ export function build30040EventSet( @@ -261,7 +260,8 @@ export function build30040EventSet(
console.log("Index event:", { documentTitle, indexDTag });
// Create section events with their metadata
const sectionEvents: NDKEvent[] = parsed.sections.map((section: any, i: number) => {
const sectionEvents: NDKEvent[] = parsed.sections.map(
(section: any, i: number) => {
const sectionDTag = `${indexDTag}-${normalizeDTagValue(section.title)}`;
console.log(`Creating section ${i}:`, {
title: section.title,
@ -285,7 +285,8 @@ export function build30040EventSet( @@ -285,7 +285,8 @@ export function build30040EventSet(
pubkey: baseEvent.pubkey,
created_at: baseEvent.created_at,
});
});
},
);
// Create proper a tags with format: kind:pubkey:d-tag
const aTags = sectionEvents.map((event) => {

5
src/lib/utils/event_search.ts

@ -10,7 +10,10 @@ import { activeInboxRelays, activeOutboxRelays } from "../ndk.ts"; @@ -10,7 +10,10 @@ import { activeInboxRelays, activeOutboxRelays } from "../ndk.ts";
/**
* Search for a single event by ID or filter
*/
export async function searchEvent(query: string, ndk: NDK): Promise<NDKEvent | null> {
export async function searchEvent(
query: string,
ndk: NDK,
): Promise<NDKEvent | null> {
if (!ndk) {
console.warn("[Search] No NDK instance available");
return null;

14
src/lib/utils/markup/advancedMarkupParser.ts

@ -406,23 +406,29 @@ function processInlineCodeMath(content: string): string { @@ -406,23 +406,29 @@ function processInlineCodeMath(content: string): string {
}
// Process display math ($$...$$) first to avoid conflicts with inline math
let processedContent = codeContent.replace(/\$\$([\s\S]*?)\$\$/g, (mathMatch: string, mathContent: string) => {
let processedContent = codeContent.replace(
/\$\$([\s\S]*?)\$\$/g,
(mathMatch: string, mathContent: string) => {
// Skip empty math expressions
if (!mathContent.trim()) {
return mathMatch;
}
return `<span class="math-display">\\[${mathContent}\\]</span>`;
});
},
);
// Process inline math ($...$) after display math
// Use a more sophisticated regex that handles escaped dollar signs
processedContent = processedContent.replace(/\$((?:[^$\\]|\\.)*?)\$/g, (mathMatch: string, mathContent: string) => {
processedContent = processedContent.replace(
/\$((?:[^$\\]|\\.)*?)\$/g,
(mathMatch: string, mathContent: string) => {
// Skip empty math expressions
if (!mathContent.trim()) {
return mathMatch;
}
return `<span class="math-inline">\\(${mathContent}\\)</span>`;
});
},
);
return `\`${processedContent}\``;
});

12
src/lib/utils/markup/basicMarkupParser.ts

@ -7,8 +7,6 @@ import { @@ -7,8 +7,6 @@ import {
processWikilinks,
} from "./markupUtils.ts";
export function preProcessBasicMarkup(text: string): string {
try {
// Process basic text formatting first
@ -26,7 +24,10 @@ export function preProcessBasicMarkup(text: string): string { @@ -26,7 +24,10 @@ export function preProcessBasicMarkup(text: string): string {
}
}
export async function postProcessBasicMarkup(text: string, ndk?: NDK): Promise<string> {
export async function postProcessBasicMarkup(
text: string,
ndk?: NDK,
): Promise<string> {
try {
// Process Nostr identifiers last
let processedText = await processNostrIdentifiersInText(text, ndk);
@ -40,7 +41,10 @@ export async function postProcessBasicMarkup(text: string, ndk?: NDK): Promise<s @@ -40,7 +41,10 @@ export async function postProcessBasicMarkup(text: string, ndk?: NDK): Promise<s
}
}
export async function parseBasicMarkup(text: string, ndk?: NDK): Promise<string> {
export async function parseBasicMarkup(
text: string,
ndk?: NDK,
): Promise<string> {
if (!text) return "";
try {

5
src/lib/utils/markup/embeddedMarkupParser.ts

@ -1,4 +1,7 @@ @@ -1,4 +1,7 @@
import { postProcessBasicMarkup, preProcessBasicMarkup } from "./basicMarkupParser.ts";
import {
postProcessBasicMarkup,
preProcessBasicMarkup,
} from "./basicMarkupParser.ts";
import { processNostrIdentifiersWithEmbeddedEvents } from "./markupUtils.ts";
/**

57
src/lib/utils/markup/markupUtils.ts

@ -80,7 +80,10 @@ export function replaceAlexandriaNostrLinks(text: string): string { @@ -80,7 +80,10 @@ export function replaceAlexandriaNostrLinks(text: string): string {
return text;
}
export function renderListGroup(lines: string[], typeHint?: "ol" | "ul"): string {
export function renderListGroup(
lines: string[],
typeHint?: "ol" | "ul",
): string {
function parseList(
start: number,
indent: number,
@ -322,7 +325,9 @@ export async function processNostrIdentifiersInText( @@ -322,7 +325,9 @@ export async function processNostrIdentifiersInText(
metadata = await getUserMetadata(identifier, ndk);
} else {
// Fallback when NDK is not available - just use the identifier
metadata = { name: identifier.slice(0, 8) + "..." + identifier.slice(-4) };
metadata = {
name: identifier.slice(0, 8) + "..." + identifier.slice(-4),
};
}
const displayText = metadata.displayName || metadata.name;
const link = createProfileLink(identifier, displayText);
@ -391,14 +396,18 @@ export function processAllNostrIdentifiers(text: string): string { @@ -391,14 +396,18 @@ export function processAllNostrIdentifiers(text: string): string {
// Pattern for prefixed nostr identifiers (nostr:npub1, nostr:note1, etc.)
// This handles both full identifiers and partial ones that might appear in content
const prefixedNostrPattern = /nostr:(npub|nprofile|note|nevent|naddr)[a-zA-Z0-9]{20,}/g;
const prefixedNostrPattern =
/nostr:(npub|nprofile|note|nevent|naddr)[a-zA-Z0-9]{20,}/g;
// Pattern for bare nostr identifiers (npub1, note1, nevent1, naddr1)
// Exclude matches that are part of URLs to avoid breaking existing links
const bareNostrPattern = /(?<!https?:\/\/[^\s]*)(?<!wss?:\/\/[^\s]*)(?<!nostr:)(npub1|note1|nevent1|naddr1)[a-zA-Z0-9]{20,}/g;
const bareNostrPattern =
/(?<!https?:\/\/[^\s]*)(?<!wss?:\/\/[^\s]*)(?<!nostr:)(npub1|note1|nevent1|naddr1)[a-zA-Z0-9]{20,}/g;
// Process prefixed nostr identifiers first
const prefixedMatches = Array.from(processedText.matchAll(prefixedNostrPattern));
const prefixedMatches = Array.from(
processedText.matchAll(prefixedNostrPattern),
);
// Process them in reverse order to avoid index shifting issues
for (let i = prefixedMatches.length - 1; i >= 0; i--) {
@ -407,11 +416,12 @@ export function processAllNostrIdentifiers(text: string): string { @@ -407,11 +416,12 @@ export function processAllNostrIdentifiers(text: string): string {
const matchIndex = match.index ?? 0;
// Create shortened display text
const identifier = fullMatch.replace('nostr:', '');
const identifier = fullMatch.replace("nostr:", "");
const displayText = `${identifier.slice(0, 8)}...${identifier.slice(-4)}`;
// Create clickable link
const replacement = `<a href="/events?id=${fullMatch}" class="text-primary-600 dark:text-primary-500 hover:underline break-all" title="${fullMatch}">${displayText}</a>`;
const replacement =
`<a href="/events?id=${fullMatch}" class="text-primary-600 dark:text-primary-500 hover:underline break-all" title="${fullMatch}">${displayText}</a>`;
// Replace the match in the text
processedText = processedText.slice(0, matchIndex) + replacement +
@ -431,7 +441,8 @@ export function processAllNostrIdentifiers(text: string): string { @@ -431,7 +441,8 @@ export function processAllNostrIdentifiers(text: string): string {
const displayText = `${fullMatch.slice(0, 8)}...${fullMatch.slice(-4)}`;
// Create clickable link with nostr: prefix for the href
const replacement = `<a href="/events?id=nostr:${fullMatch}" class="text-primary-600 dark:text-primary-500 hover:underline break-all" title="nostr:${fullMatch}">${displayText}</a>`;
const replacement =
`<a href="/events?id=nostr:${fullMatch}" class="text-primary-600 dark:text-primary-500 hover:underline break-all" title="nostr:${fullMatch}">${displayText}</a>`;
// Replace the match in the text
processedText = processedText.slice(0, matchIndex) + replacement +
@ -439,8 +450,11 @@ export function processAllNostrIdentifiers(text: string): string { @@ -439,8 +450,11 @@ export function processAllNostrIdentifiers(text: string): string {
}
// Also handle any remaining truncated prefixed identifiers that might be cut off or incomplete
const truncatedPrefixedPattern = /nostr:(npub|nprofile|note|nevent|naddr)[a-zA-Z0-9]{8,}/g;
const truncatedPrefixedMatches = Array.from(processedText.matchAll(truncatedPrefixedPattern));
const truncatedPrefixedPattern =
/nostr:(npub|nprofile|note|nevent|naddr)[a-zA-Z0-9]{8,}/g;
const truncatedPrefixedMatches = Array.from(
processedText.matchAll(truncatedPrefixedPattern),
);
for (let i = truncatedPrefixedMatches.length - 1; i >= 0; i--) {
const match = truncatedPrefixedMatches[i];
@ -451,11 +465,14 @@ export function processAllNostrIdentifiers(text: string): string { @@ -451,11 +465,14 @@ export function processAllNostrIdentifiers(text: string): string {
if (fullMatch.length >= 30) continue; // Full identifiers are at least 30 chars
// Create display text for truncated identifiers
const identifier = fullMatch.replace('nostr:', '');
const displayText = identifier.length > 12 ? `${identifier.slice(0, 8)}...${identifier.slice(-4)}` : identifier;
const identifier = fullMatch.replace("nostr:", "");
const displayText = identifier.length > 12
? `${identifier.slice(0, 8)}...${identifier.slice(-4)}`
: identifier;
// Create clickable link
const replacement = `<a href="/events?id=${fullMatch}" class="text-primary-600 dark:text-primary-500 hover:underline break-all" title="${fullMatch}">${displayText}</a>`;
const replacement =
`<a href="/events?id=${fullMatch}" class="text-primary-600 dark:text-primary-500 hover:underline break-all" title="${fullMatch}">${displayText}</a>`;
// Replace the match in the text
processedText = processedText.slice(0, matchIndex) + replacement +
@ -463,8 +480,11 @@ export function processAllNostrIdentifiers(text: string): string { @@ -463,8 +480,11 @@ export function processAllNostrIdentifiers(text: string): string {
}
// Handle truncated bare identifiers
const truncatedBarePattern = /(?<!https?:\/\/[^\s]*)(?<!wss?:\/\/[^\s]*)(?<!nostr:)(npub1|note1|nevent1|naddr1)[a-zA-Z0-9]{8,}/g;
const truncatedBareMatches = Array.from(processedText.matchAll(truncatedBarePattern));
const truncatedBarePattern =
/(?<!https?:\/\/[^\s]*)(?<!wss?:\/\/[^\s]*)(?<!nostr:)(npub1|note1|nevent1|naddr1)[a-zA-Z0-9]{8,}/g;
const truncatedBareMatches = Array.from(
processedText.matchAll(truncatedBarePattern),
);
for (let i = truncatedBareMatches.length - 1; i >= 0; i--) {
const match = truncatedBareMatches[i];
@ -475,10 +495,13 @@ export function processAllNostrIdentifiers(text: string): string { @@ -475,10 +495,13 @@ export function processAllNostrIdentifiers(text: string): string {
if (fullMatch.length >= 30) continue; // Full identifiers are at least 30 chars
// Create display text for truncated identifiers
const displayText = fullMatch.length > 12 ? `${fullMatch.slice(0, 8)}...${fullMatch.slice(-4)}` : fullMatch;
const displayText = fullMatch.length > 12
? `${fullMatch.slice(0, 8)}...${fullMatch.slice(-4)}`
: fullMatch;
// Create clickable link
const replacement = `<a href="/events?id=nostr:${fullMatch}" class="text-primary-600 dark:text-primary-500 hover:underline break-all" title="nostr:${fullMatch}">${displayText}</a>`;
const replacement =
`<a href="/events?id=nostr:${fullMatch}" class="text-primary-600 dark:text-primary-500 hover:underline break-all" title="nostr:${fullMatch}">${displayText}</a>`;
// Replace the match in the text
processedText = processedText.slice(0, matchIndex) + replacement +

6
src/lib/utils/nostrUtils.ts

@ -7,9 +7,9 @@ import type { Filter } from "./search_types.ts"; @@ -7,9 +7,9 @@ import type { Filter } from "./search_types.ts";
import {
anonymousRelays,
communityRelays,
localRelays,
searchRelays,
secondaryRelays,
localRelays,
} from "../consts.ts";
import { activeInboxRelays, activeOutboxRelays } from "../ndk.ts";
import { NDKRelaySet as NDKRelaySetFromNDK } from "@nostr-dev-kit/ndk";
@ -220,7 +220,7 @@ export async function processNostrIdentifiers( @@ -220,7 +220,7 @@ export async function processNostrIdentifiers(
];
const combinedContext = beforeContext + afterContext;
return urlPatterns.some(pattern => pattern.test(combinedContext));
return urlPatterns.some((pattern) => pattern.test(combinedContext));
}
// Process profiles (npub and nprofile)
@ -528,7 +528,7 @@ export async function fetchEventWithFallback( @@ -528,7 +528,7 @@ export async function fetchEventWithFallback(
* Converts various Nostr identifiers to npub format.
* Handles hex pubkeys, npub strings, and nprofile strings.
*/
export function toNpub(pubkey: string | undefined): string | null {
export function toNpub(pubkey: string | undefined): string | null {
if (!pubkey) return null;
try {
// If it's already an npub, return it

67
src/lib/utils/npubCache.ts

@ -59,7 +59,11 @@ class UnifiedProfileCache { @@ -59,7 +59,11 @@ class UnifiedProfileCache {
/**
* Get profile data, fetching fresh data if needed
*/
async getProfile(identifier: string, ndk?: NDK, force = false): Promise<NpubMetadata> {
async getProfile(
identifier: string,
ndk?: NDK,
force = false,
): Promise<NpubMetadata> {
const cleanId = identifier.replace(/^nostr:/, "");
// Check cache first (unless forced)
@ -81,8 +85,13 @@ class UnifiedProfileCache { @@ -81,8 +85,13 @@ class UnifiedProfileCache {
/**
* Fetch profile from all available relays and cache it
*/
private async fetchAndCacheProfile(identifier: string, ndk?: NDK): Promise<NpubMetadata> {
const fallback = { name: `${identifier.slice(0, 8)}...${identifier.slice(-4)}` };
private async fetchAndCacheProfile(
identifier: string,
ndk?: NDK,
): Promise<NpubMetadata> {
const fallback = {
name: `${identifier.slice(0, 8)}...${identifier.slice(-4)}`,
};
try {
if (!ndk) {
@ -92,7 +101,10 @@ class UnifiedProfileCache { @@ -92,7 +101,10 @@ class UnifiedProfileCache {
const decoded = nip19.decode(identifier);
if (!decoded) {
console.warn("UnifiedProfileCache: Failed to decode identifier:", identifier);
console.warn(
"UnifiedProfileCache: Failed to decode identifier:",
identifier,
);
return fallback;
}
@ -103,11 +115,17 @@ class UnifiedProfileCache { @@ -103,11 +115,17 @@ class UnifiedProfileCache {
} else if (decoded.type === "nprofile") {
pubkey = decoded.data.pubkey;
} else {
console.warn("UnifiedProfileCache: Unsupported identifier type:", decoded.type);
console.warn(
"UnifiedProfileCache: Unsupported identifier type:",
decoded.type,
);
return fallback;
}
console.log("UnifiedProfileCache: Fetching fresh profile for pubkey:", pubkey);
console.log(
"UnifiedProfileCache: Fetching fresh profile for pubkey:",
pubkey,
);
// Use fetchEventWithFallback to search ALL available relays
const profileEvent = await fetchEventWithFallback(ndk, {
@ -116,7 +134,10 @@ class UnifiedProfileCache { @@ -116,7 +134,10 @@ class UnifiedProfileCache {
});
if (!profileEvent || !profileEvent.content) {
console.warn("UnifiedProfileCache: No profile event found for:", pubkey);
console.warn(
"UnifiedProfileCache: No profile event found for:",
pubkey,
);
return fallback;
}
@ -147,7 +168,6 @@ class UnifiedProfileCache { @@ -147,7 +168,6 @@ class UnifiedProfileCache {
console.log("UnifiedProfileCache: Cached fresh profile:", metadata);
return metadata;
} catch (e) {
console.error("UnifiedProfileCache: Error fetching profile:", e);
return fallback;
@ -177,7 +197,12 @@ class UnifiedProfileCache { @@ -177,7 +197,12 @@ class UnifiedProfileCache {
/**
* Set profile data in cache
*/
set(identifier: string, profile: NpubMetadata, pubkey?: string, relaySource?: string): void {
set(
identifier: string,
profile: NpubMetadata,
pubkey?: string,
relaySource?: string,
): void {
const cleanId = identifier.replace(/^nostr:/, "");
const entry: CacheEntry = {
profile,
@ -271,11 +296,13 @@ class UnifiedProfileCache { @@ -271,11 +296,13 @@ class UnifiedProfileCache {
}
}
expiredKeys.forEach(key => this.cache.delete(key));
expiredKeys.forEach((key) => this.cache.delete(key));
if (expiredKeys.length > 0) {
this.saveToStorage();
console.log(`UnifiedProfileCache: Cleaned up ${expiredKeys.length} expired entries`);
console.log(
`UnifiedProfileCache: Cleaned up ${expiredKeys.length} expired entries`,
);
}
}
}
@ -294,7 +321,8 @@ if (typeof window !== "undefined") { @@ -294,7 +321,8 @@ if (typeof window !== "undefined") {
// but make it use the unified cache internally
export const npubCache = {
get: (key: string) => unifiedProfileCache.getCached(key),
set: (key: string, value: NpubMetadata) => unifiedProfileCache.set(key, value),
set: (key: string, value: NpubMetadata) =>
unifiedProfileCache.set(key, value),
has: (key: string) => unifiedProfileCache.has(key),
delete: (key: string) => unifiedProfileCache.delete(key),
clear: () => unifiedProfileCache.clear(),
@ -303,14 +331,19 @@ export const npubCache = { @@ -303,14 +331,19 @@ export const npubCache = {
};
// Legacy compatibility for old profileCache functions
export async function getDisplayName(pubkey: string, ndk: NDK): Promise<string> {
export async function getDisplayName(
pubkey: string,
ndk: NDK,
): Promise<string> {
const profile = await unifiedProfileCache.getProfile(pubkey, ndk);
return profile.displayName || profile.name || `${pubkey.slice(0, 8)}...${pubkey.slice(-4)}`;
return profile.displayName || profile.name ||
`${pubkey.slice(0, 8)}...${pubkey.slice(-4)}`;
}
export function getDisplayNameSync(pubkey: string): string {
const profile = unifiedProfileCache.getCached(pubkey);
return profile?.displayName || profile?.name || `${pubkey.slice(0, 8)}...${pubkey.slice(-4)}`;
return profile?.displayName || profile?.name ||
`${pubkey.slice(0, 8)}...${pubkey.slice(-4)}`;
}
export async function batchFetchProfiles(
@ -340,8 +373,8 @@ export async function batchFetchProfiles( @@ -340,8 +373,8 @@ export async function batchFetchProfiles(
});
const results = await Promise.allSettled(fetchPromises);
results.forEach(result => {
if (result.status === 'fulfilled' && result.value) {
results.forEach((result) => {
if (result.status === "fulfilled" && result.value) {
allProfileEvents.push(result.value);
}
});

37
src/lib/utils/profile_search.ts

@ -1,8 +1,17 @@ @@ -1,8 +1,17 @@
import { activeInboxRelays, activeOutboxRelays } from "../ndk.ts";
import { getNpubFromNip05, getUserMetadata, fetchEventWithFallback } from "./nostrUtils.ts";
import {
fetchEventWithFallback,
getNpubFromNip05,
getUserMetadata,
} from "./nostrUtils.ts";
import NDK, { NDKEvent, NDKRelaySet } from "@nostr-dev-kit/ndk";
import { searchCache } from "./searchCache.ts";
import { communityRelays, searchRelays, secondaryRelays, anonymousRelays } from "../consts.ts";
import {
anonymousRelays,
communityRelays,
searchRelays,
secondaryRelays,
} from "../consts.ts";
import { get } from "svelte/store";
import type { NostrProfile, ProfileSearchResult } from "./search_types.ts";
import {
@ -94,7 +103,10 @@ export async function searchProfiles( @@ -94,7 +103,10 @@ export async function searchProfiles(
}
}
} catch (e) {
console.warn("profile_search: Failed to fetch original event timestamp:", e);
console.warn(
"profile_search: Failed to fetch original event timestamp:",
e,
);
}
const profile: NostrProfile & { created_at?: number } = {
@ -223,7 +235,10 @@ async function searchNip05Domains( @@ -223,7 +235,10 @@ async function searchNip05Domains(
}
}
} catch (e) {
console.warn("profile_search: Failed to fetch original event timestamp:", e);
console.warn(
"profile_search: Failed to fetch original event timestamp:",
e,
);
}
const profile: NostrProfile & { created_at?: number } = {
@ -275,7 +290,10 @@ async function searchNip05Domains( @@ -275,7 +290,10 @@ async function searchNip05Domains(
}
}
} catch (e) {
console.warn("profile_search: Failed to fetch original event timestamp:", e);
console.warn(
"profile_search: Failed to fetch original event timestamp:",
e,
);
}
const profile: NostrProfile & { created_at?: number } = {
@ -330,7 +348,9 @@ async function quickRelaySearch( @@ -330,7 +348,9 @@ async function quickRelaySearch(
// This ensures we don't miss profiles due to stale cache or limited relay coverage
// Get all available relays from NDK pool (most comprehensive)
const poolRelays = Array.from(ndk.pool.relays.values()).map((r: any) => r.url) as string[];
const poolRelays = Array.from(ndk.pool.relays.values()).map((r: any) =>
r.url
) as string[];
const userInboxRelays = get(activeInboxRelays);
const userOutboxRelays = get(activeOutboxRelays);
@ -347,7 +367,10 @@ async function quickRelaySearch( @@ -347,7 +367,10 @@ async function quickRelaySearch(
// Deduplicate relay URLs
const uniqueRelayUrls = [...new Set(allRelayUrls)];
console.log("Using ALL available relays for profile search:", uniqueRelayUrls);
console.log(
"Using ALL available relays for profile search:",
uniqueRelayUrls,
);
console.log("Total relays for profile search:", uniqueRelayUrls.length);
// Create relay sets for parallel search

226
src/lib/utils/subscription_search.ts

@ -58,7 +58,7 @@ async function prioritizeSearchEvents( @@ -58,7 +58,7 @@ async function prioritizeSearchEvents(
events: NDKEvent[],
targetPubkey?: string,
maxResults: number = SEARCH_LIMITS.GENERAL_CONTENT,
ndk?: NDK
ndk?: NDK,
): Promise<NDKEvent[]> {
if (events.length === 0) {
return [];
@ -72,7 +72,9 @@ async function prioritizeSearchEvents( @@ -72,7 +72,9 @@ async function prioritizeSearchEvents(
if (ndk) {
try {
// Import user list functions dynamically to avoid circular dependencies
const { fetchCurrentUserLists, getPubkeysFromListKind } = await import("./user_lists.ts");
const { fetchCurrentUserLists, getPubkeysFromListKind } = await import(
"./user_lists.ts"
);
const { checkCommunity } = await import("./community_checker.ts");
// Get current user's follow lists (if logged in)
@ -80,10 +82,14 @@ async function prioritizeSearchEvents( @@ -80,10 +82,14 @@ async function prioritizeSearchEvents(
userFollowPubkeys = getPubkeysFromListKind(userLists, 3); // Kind 3 = follow list
// Check community status for unique pubkeys in events (limit to prevent hanging)
const uniquePubkeys = new Set(events.map(e => e.pubkey).filter(Boolean));
const uniquePubkeys = new Set(
events.map((e) => e.pubkey).filter(Boolean),
);
const pubkeysToCheck = Array.from(uniquePubkeys).slice(0, 20); // Limit to first 20 pubkeys
console.log(`subscription_search: Checking community status for ${pubkeysToCheck.length} pubkeys out of ${uniquePubkeys.size} total`);
console.log(
`subscription_search: Checking community status for ${pubkeysToCheck.length} pubkeys out of ${uniquePubkeys.size} total`,
);
const communityChecks = await Promise.allSettled(
pubkeysToCheck.map(async (pubkey) => {
@ -91,19 +97,25 @@ async function prioritizeSearchEvents( @@ -91,19 +97,25 @@ async function prioritizeSearchEvents(
const isCommunityMember = await Promise.race([
checkCommunity(pubkey),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Community check timeout')), 2000)
setTimeout(
() => reject(new Error("Community check timeout")),
2000,
)
),
]);
return { pubkey, isCommunityMember };
} catch (error) {
console.warn(`subscription_search: Community check failed for ${pubkey}:`, error);
console.warn(
`subscription_search: Community check failed for ${pubkey}:`,
error,
);
return { pubkey, isCommunityMember: false };
}
})
}),
);
// Build set of community member pubkeys
communityChecks.forEach(result => {
communityChecks.forEach((result) => {
if (result.status === "fulfilled" && result.value.isCommunityMember) {
communityMemberPubkeys.add(result.value.pubkey);
}
@ -112,13 +124,18 @@ async function prioritizeSearchEvents( @@ -112,13 +124,18 @@ async function prioritizeSearchEvents(
console.log("subscription_search: Prioritization data loaded:", {
userFollows: userFollowPubkeys.size,
communityMembers: communityMemberPubkeys.size,
totalEvents: events.length
totalEvents: events.length,
});
} catch (error) {
console.warn("subscription_search: Failed to load prioritization data:", error);
console.warn(
"subscription_search: Failed to load prioritization data:",
error,
);
}
} else {
console.log("subscription_search: No NDK provided, skipping user list and community checks");
console.log(
"subscription_search: No NDK provided, skipping user list and community checks",
);
}
// Separate events into priority tiers
@ -131,7 +148,9 @@ async function prioritizeSearchEvents( @@ -131,7 +148,9 @@ async function prioritizeSearchEvents(
const isFromTarget = targetPubkey && event.pubkey === targetPubkey;
const isPrioritizedKind = PRIORITIZED_EVENT_KINDS.has(event.kind || 0);
const isFromFollow = userFollowPubkeys.has(event.pubkey || "");
const isFromCommunityMember = communityMemberPubkeys.has(event.pubkey || "");
const isFromCommunityMember = communityMemberPubkeys.has(
event.pubkey || "",
);
// AI-NOTE: Prioritized kinds are always in tier 1
// Target pubkey priority only applies to n: searches (when targetPubkey is provided)
@ -181,7 +200,7 @@ async function prioritizeSearchEvents( @@ -181,7 +200,7 @@ async function prioritizeSearchEvents(
tier2: tier2.length, // User follows
tier3: tier3.length, // Community members
tier4: tier4.length, // Others
total: result.length
total: result.length,
});
return result;
@ -224,25 +243,34 @@ export async function searchBySubscription( @@ -224,25 +243,34 @@ export async function searchBySubscription(
// AI-NOTE: Ensure cached events have created_at property preserved
// This fixes the "Unknown date" issue when events are retrieved from cache
const eventsWithCreatedAt = cachedResult.events.map(event => {
if (event && typeof event === 'object' && !event.created_at) {
console.warn("subscription_search: Event missing created_at, setting to 0:", event.id);
const eventsWithCreatedAt = cachedResult.events.map((event) => {
if (event && typeof event === "object" && !event.created_at) {
console.warn(
"subscription_search: Event missing created_at, setting to 0:",
event.id,
);
(event as any).created_at = 0;
}
return event;
});
const secondOrderWithCreatedAt = cachedResult.secondOrder.map(event => {
if (event && typeof event === 'object' && !event.created_at) {
console.warn("subscription_search: Second order event missing created_at, setting to 0:", event.id);
const secondOrderWithCreatedAt = cachedResult.secondOrder.map((event) => {
if (event && typeof event === "object" && !event.created_at) {
console.warn(
"subscription_search: Second order event missing created_at, setting to 0:",
event.id,
);
(event as any).created_at = 0;
}
return event;
});
const tTagEventsWithCreatedAt = cachedResult.tTagEvents.map(event => {
if (event && typeof event === 'object' && !event.created_at) {
console.warn("subscription_search: T-tag event missing created_at, setting to 0:", event.id);
const tTagEventsWithCreatedAt = cachedResult.tTagEvents.map((event) => {
if (event && typeof event === "object" && !event.created_at) {
console.warn(
"subscription_search: T-tag event missing created_at, setting to 0:",
event.id,
);
(event as any).created_at = 0;
}
return event;
@ -252,18 +280,22 @@ export async function searchBySubscription( @@ -252,18 +280,22 @@ export async function searchBySubscription(
...cachedResult,
events: eventsWithCreatedAt,
secondOrder: secondOrderWithCreatedAt,
tTagEvents: tTagEventsWithCreatedAt
tTagEvents: tTagEventsWithCreatedAt,
};
// AI-NOTE: Return cached results immediately but trigger second-order search in background
// This ensures we get fast results while still updating second-order data
console.log("subscription_search: Returning cached result immediately, triggering background second-order search");
console.log(
"subscription_search: Returning cached result immediately, triggering background second-order search",
);
// Trigger second-order search in background for all search types
if (ndk) {
// Start second-order search in background for n and d searches only
if (searchType === "n" || searchType === "d") {
console.log("subscription_search: Triggering background second-order search for cached result");
console.log(
"subscription_search: Triggering background second-order search for cached result",
);
performSecondOrderSearchInBackground(
searchType as "n" | "d",
eventsWithCreatedAt,
@ -271,7 +303,7 @@ export async function searchBySubscription( @@ -271,7 +303,7 @@ export async function searchBySubscription(
cachedResult.addresses || new Set(),
ndk,
searchType === "n" ? eventsWithCreatedAt[0]?.pubkey : undefined,
callbacks
callbacks,
);
}
}
@ -316,7 +348,10 @@ export async function searchBySubscription( @@ -316,7 +348,10 @@ export async function searchBySubscription(
// AI-NOTE: Check for preloaded events first (for profile searches)
if (searchFilter.preloadedEvents && searchFilter.preloadedEvents.length > 0) {
console.log("subscription_search: Using preloaded events:", searchFilter.preloadedEvents.length);
console.log(
"subscription_search: Using preloaded events:",
searchFilter.preloadedEvents.length,
);
processPrimaryRelayResults(
new Set(searchFilter.preloadedEvents),
searchType,
@ -328,7 +363,9 @@ export async function searchBySubscription( @@ -328,7 +363,9 @@ export async function searchBySubscription(
);
if (hasResults(searchState, searchType)) {
console.log("subscription_search: Found results from preloaded events, returning immediately");
console.log(
"subscription_search: Found results from preloaded events, returning immediately",
);
const immediateResult = createSearchResult(
searchState,
searchType,
@ -376,10 +413,16 @@ export async function searchBySubscription( @@ -376,10 +413,16 @@ export async function searchBySubscription(
);
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error("Primary relay search timeout")), TIMEOUTS.SUBSCRIPTION_SEARCH);
setTimeout(
() => reject(new Error("Primary relay search timeout")),
TIMEOUTS.SUBSCRIPTION_SEARCH,
);
});
const primaryEvents = await Promise.race([primaryEventsPromise, timeoutPromise]) as any;
const primaryEvents = await Promise.race([
primaryEventsPromise,
timeoutPromise,
]) as any;
console.log(
"subscription_search: Primary relay returned",
@ -473,10 +516,16 @@ export async function searchBySubscription( @@ -473,10 +516,16 @@ export async function searchBySubscription(
);
const fallbackTimeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error("Fallback search timeout")), TIMEOUTS.SUBSCRIPTION_SEARCH);
setTimeout(
() => reject(new Error("Fallback search timeout")),
TIMEOUTS.SUBSCRIPTION_SEARCH,
);
});
const fallbackEvents = await Promise.race([fallbackEventsPromise, fallbackTimeoutPromise]) as any;
const fallbackEvents = await Promise.race([
fallbackEventsPromise,
fallbackTimeoutPromise,
]) as any;
console.log(
"subscription_search: Fallback search returned",
@ -520,8 +569,13 @@ export async function searchBySubscription( @@ -520,8 +569,13 @@ export async function searchBySubscription(
);
// If it's a timeout error, continue to return empty result
if (fallbackError instanceof Error && fallbackError.message.includes("timeout")) {
console.log("subscription_search: Fallback search timed out, returning empty result");
if (
fallbackError instanceof Error &&
fallbackError.message.includes("timeout")
) {
console.log(
"subscription_search: Fallback search timed out, returning empty result",
);
}
}
@ -556,7 +610,9 @@ export async function searchBySubscription( @@ -556,7 +610,9 @@ export async function searchBySubscription(
// If it's a timeout error, continue to Phase 2 instead of failing
if (error instanceof Error && error.message.includes("timeout")) {
console.log("subscription_search: Primary relay search timed out, continuing to Phase 2");
console.log(
"subscription_search: Primary relay search timed out, continuing to Phase 2",
);
} else {
// For other errors, we might want to fail the search
throw error;
@ -685,7 +741,11 @@ async function createSearchFilter( @@ -685,7 +741,11 @@ async function createSearchFilter(
hexPubkey = decoded.data as string;
}
} catch (e) {
console.warn("subscription_search: Failed to decode npub:", profile.pubkey, e);
console.warn(
"subscription_search: Failed to decode npub:",
profile.pubkey,
e,
);
}
}
event.pubkey = hexPubkey;
@ -695,11 +755,17 @@ async function createSearchFilter( @@ -695,11 +755,17 @@ async function createSearchFilter(
// This ensures the profile cards show the actual creation date instead of "Unknown date"
if ((profile as any).created_at) {
event.created_at = (profile as any).created_at;
console.log("subscription_search: Using preserved timestamp:", event.created_at);
console.log(
"subscription_search: Using preserved timestamp:",
event.created_at,
);
} else {
// Fallback to current timestamp if no preserved timestamp
event.created_at = Math.floor(Date.now() / 1000);
console.log("subscription_search: Using fallback timestamp:", event.created_at);
console.log(
"subscription_search: Using fallback timestamp:",
event.created_at,
);
}
return event;
@ -712,7 +778,10 @@ async function createSearchFilter( @@ -712,7 +778,10 @@ async function createSearchFilter(
searchTerm: normalizedSearchTerm,
preloadedEvents: events, // AI-NOTE: Pass preloaded events
};
console.log("subscription_search: Created profile filter with preloaded events:", nFilter);
console.log(
"subscription_search: Created profile filter with preloaded events:",
nFilter,
);
return nFilter;
}
default: {
@ -721,8 +790,6 @@ async function createSearchFilter( @@ -721,8 +790,6 @@ async function createSearchFilter(
}
}
/**
* Create primary relay set for search operations
* AI-NOTE: Updated to use all available relays to prevent search failures
@ -816,7 +883,9 @@ function processPrimaryRelayResults( @@ -816,7 +883,9 @@ function processPrimaryRelayResults(
for (const event of events) {
// Check if we've reached the event limit
if (processedCount >= maxEvents) {
console.log(`subscription_search: Reached event limit of ${maxEvents} in primary relay processing`);
console.log(
`subscription_search: Reached event limit of ${maxEvents} in primary relay processing`,
);
break;
}
@ -1029,7 +1098,9 @@ function searchOtherRelaysInBackground( @@ -1029,7 +1098,9 @@ function searchOtherRelaysInBackground(
sub.on("event", (event: NDKEvent) => {
// Check if we've reached the event limit
if (eventCount >= maxEvents) {
console.log(`subscription_search: Reached event limit of ${maxEvents}, stopping event processing`);
console.log(
`subscription_search: Reached event limit of ${maxEvents}, stopping event processing`,
);
sub.stop();
return;
}
@ -1058,7 +1129,9 @@ function searchOtherRelaysInBackground( @@ -1058,7 +1129,9 @@ function searchOtherRelaysInBackground(
// Add timeout to prevent hanging
const timeoutId = setTimeout(async () => {
if (!resolved) {
console.log("subscription_search: Background search timeout, resolving with current results");
console.log(
"subscription_search: Background search timeout, resolving with current results",
);
resolved = true;
sub.stop();
const result = await processEoseResults(
@ -1106,7 +1179,12 @@ async function processEoseResults( @@ -1106,7 +1179,12 @@ async function processEoseResults(
if (searchType === "n") {
return processProfileEoseResults(searchState, searchFilter, ndk, callbacks);
} else if (searchType === "d") {
return await processContentEoseResults(searchState, searchType, ndk, callbacks);
return await processContentEoseResults(
searchState,
searchType,
ndk,
callbacks,
);
} else if (searchType === "t") {
return await processTTagEoseResults(searchState, ndk);
}
@ -1242,7 +1320,7 @@ async function processContentEoseResults( @@ -1242,7 +1320,7 @@ async function processContentEoseResults(
dedupedEvents,
undefined, // No specific target pubkey for d-tag searches
SEARCH_LIMITS.GENERAL_CONTENT,
ndk
ndk,
);
// AI-NOTE: Attach profile data to first-order events for display
@ -1276,7 +1354,10 @@ async function processContentEoseResults( @@ -1276,7 +1354,10 @@ async function processContentEoseResults(
/**
* Process t-tag EOSE results
*/
async function processTTagEoseResults(searchState: any, ndk?: NDK): Promise<SearchResult> {
async function processTTagEoseResults(
searchState: any,
ndk?: NDK,
): Promise<SearchResult> {
if (searchState.tTagEvents.length === 0) {
return createEmptySearchResult("t", searchState.normalizedSearchTerm);
}
@ -1287,7 +1368,7 @@ async function processTTagEoseResults(searchState: any, ndk?: NDK): Promise<Sear @@ -1287,7 +1368,7 @@ async function processTTagEoseResults(searchState: any, ndk?: NDK): Promise<Sear
searchState.tTagEvents,
undefined, // No specific target pubkey for t-tag searches
SEARCH_LIMITS.GENERAL_CONTENT,
ndk
ndk,
);
// AI-NOTE: Attach profile data to t-tag events for display
@ -1460,7 +1541,9 @@ async function performSecondOrderSearchInBackground( @@ -1460,7 +1541,9 @@ async function performSecondOrderSearchInBackground(
await Promise.race([fetchPromise, fetchTimeoutPromise]);
// Now do the prioritization without timeout
console.log("subscription_search: Event fetching completed, starting prioritization...");
console.log(
"subscription_search: Event fetching completed, starting prioritization...",
);
// Deduplicate by event ID
const uniqueSecondOrder = new Map<string, NDKEvent>();
@ -1484,18 +1567,18 @@ async function performSecondOrderSearchInBackground( @@ -1484,18 +1567,18 @@ async function performSecondOrderSearchInBackground(
deduplicatedSecondOrder,
targetPubkey,
SEARCH_LIMITS.SECOND_ORDER_RESULTS,
ndk
ndk,
);
const prioritizationTimeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Prioritization timeout')), 15000); // 15 second timeout
setTimeout(() => reject(new Error("Prioritization timeout")), 15000); // 15 second timeout
});
let prioritizedSecondOrder: NDKEvent[];
try {
prioritizedSecondOrder = await Promise.race([
prioritizationPromise,
prioritizationTimeoutPromise
prioritizationTimeoutPromise,
]) as NDKEvent[];
console.log(
@ -1504,7 +1587,10 @@ async function performSecondOrderSearchInBackground( @@ -1504,7 +1587,10 @@ async function performSecondOrderSearchInBackground(
"prioritized results",
);
} catch (error) {
console.warn("subscription_search: Prioritization failed, using simple sorting:", error);
console.warn(
"subscription_search: Prioritization failed, using simple sorting:",
error,
);
// Fallback to simple sorting if prioritization fails
prioritizedSecondOrder = deduplicatedSecondOrder.sort((a, b) => {
// Prioritize events from target pubkey first (for n: searches)
@ -1577,16 +1663,23 @@ async function performSecondOrderSearchInBackground( @@ -1577,16 +1663,23 @@ async function performSecondOrderSearchInBackground(
* @param ndk NDK instance for fetching profile data
* @returns Promise that resolves when profile data is attached
*/
async function attachProfileDataToEvents(events: NDKEvent[], ndk: NDK): Promise<void> {
async function attachProfileDataToEvents(
events: NDKEvent[],
ndk: NDK,
): Promise<void> {
if (events.length === 0) {
return;
}
console.log(`subscription_search: Attaching profile data to ${events.length} events`);
console.log(
`subscription_search: Attaching profile data to ${events.length} events`,
);
try {
// Import user list functions dynamically to avoid circular dependencies
const { fetchCurrentUserLists, isPubkeyInUserLists } = await import("./user_lists.ts");
const { fetchCurrentUserLists, isPubkeyInUserLists } = await import(
"./user_lists.ts"
);
// Get current user's lists for user list status
const userLists = await fetchCurrentUserLists(undefined, ndk);
@ -1599,14 +1692,18 @@ async function attachProfileDataToEvents(events: NDKEvent[], ndk: NDK): Promise< @@ -1599,14 +1692,18 @@ async function attachProfileDataToEvents(events: NDKEvent[], ndk: NDK): Promise<
}
});
console.log(`subscription_search: Found ${uniquePubkeys.size} unique pubkeys to fetch profiles for`);
console.log(
`subscription_search: Found ${uniquePubkeys.size} unique pubkeys to fetch profiles for`,
);
// Fetch profile data for each unique pubkey
const profilePromises = Array.from(uniquePubkeys).map(async (pubkey) => {
try {
// Import getUserMetadata dynamically to avoid circular dependencies
const { getUserMetadata } = await import("./nostrUtils.ts");
const npub = await import("./nostrUtils.ts").then(m => m.toNpub(pubkey));
const npub = await import("./nostrUtils.ts").then((m) =>
m.toNpub(pubkey)
);
if (npub) {
const profileData = await getUserMetadata(npub, ndk, true);
@ -1619,13 +1716,16 @@ async function attachProfileDataToEvents(events: NDKEvent[], ndk: NDK): Promise< @@ -1619,13 +1716,16 @@ async function attachProfileDataToEvents(events: NDKEvent[], ndk: NDK): Promise<
pubkey,
profileData: {
...profileData,
isInUserLists: isInLists
}
isInUserLists: isInLists,
},
};
}
}
} catch (error) {
console.warn(`subscription_search: Failed to fetch profile for ${pubkey}:`, error);
console.warn(
`subscription_search: Failed to fetch profile for ${pubkey}:`,
error,
);
}
return null;
});
@ -1640,7 +1740,9 @@ async function attachProfileDataToEvents(events: NDKEvent[], ndk: NDK): Promise< @@ -1640,7 +1740,9 @@ async function attachProfileDataToEvents(events: NDKEvent[], ndk: NDK): Promise<
}
});
console.log(`subscription_search: Successfully fetched ${profileMap.size} profiles`);
console.log(
`subscription_search: Successfully fetched ${profileMap.size} profiles`,
);
// Attach profile data to each event
events.forEach((event) => {

73
src/lib/utils/user_lists.ts

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
import { getNdkContext, activeInboxRelays } from "../ndk.ts";
import { activeInboxRelays, getNdkContext } from "../ndk.ts";
import { get } from "svelte/store";
import type { NDKEvent } from "@nostr-dev-kit/ndk";
import type NDK from "@nostr-dev-kit/ndk";
@ -52,7 +52,7 @@ export interface UserListEvent { @@ -52,7 +52,7 @@ export interface UserListEvent {
export async function fetchUserLists(
pubkey: string,
listKinds: number[] = [...PEOPLE_LIST_KINDS],
ndk?: NDK
ndk?: NDK,
): Promise<UserListEvent[]> {
const ndkInstance = ndk || getNdkContext();
if (!ndkInstance) {
@ -60,7 +60,10 @@ export async function fetchUserLists( @@ -60,7 +60,10 @@ export async function fetchUserLists(
return [];
}
console.log(`fetchUserLists: Fetching lists for ${pubkey}, kinds:`, listKinds);
console.log(
`fetchUserLists: Fetching lists for ${pubkey}, kinds:`,
listKinds,
);
try {
const events = await ndkInstance.fetchEvents({
@ -74,8 +77,8 @@ export async function fetchUserLists( @@ -74,8 +77,8 @@ export async function fetchUserLists(
const pubkeys: string[] = [];
// Extract pubkeys from p-tags
event.tags.forEach(tag => {
if (tag[0] === 'p' && tag[1]) {
event.tags.forEach((tag) => {
if (tag[0] === "p" && tag[1]) {
pubkeys.push(tag[1]);
}
});
@ -96,7 +99,7 @@ export async function fetchUserLists( @@ -96,7 +99,7 @@ export async function fetchUserLists(
// Get list name from d-tag if available (for addressable lists)
if (!listName && event.kind >= 30000 && event.kind < 40000) {
const dTag = event.getMatchingTags('d')[0]?.[1];
const dTag = event.getMatchingTags("d")[0]?.[1];
if (dTag) {
listName = dTag;
}
@ -111,7 +114,11 @@ export async function fetchUserLists( @@ -111,7 +114,11 @@ export async function fetchUserLists(
});
}
console.log(`fetchUserLists: Found ${userLists.length} lists with ${userLists.reduce((sum, list) => sum + list.pubkeys.length, 0)} total pubkeys`);
console.log(
`fetchUserLists: Found ${userLists.length} lists with ${
userLists.reduce((sum, list) => sum + list.pubkeys.length, 0)
} total pubkeys`,
);
return userLists;
} catch (error) {
console.error("fetchUserLists: Error fetching user lists:", error);
@ -127,7 +134,7 @@ export async function fetchUserLists( @@ -127,7 +134,7 @@ export async function fetchUserLists(
*/
export async function fetchCurrentUserLists(
listKinds: number[] = [...PEOPLE_LIST_KINDS],
ndk?: NDK
ndk?: NDK,
): Promise<UserListEvent[]> {
const userState = get(userStore);
@ -145,11 +152,13 @@ export async function fetchCurrentUserLists( @@ -145,11 +152,13 @@ export async function fetchCurrentUserLists(
* @param userLists - Array of UserListEvent objects
* @returns Set of unique pubkeys
*/
export function getPubkeysFromUserLists(userLists: UserListEvent[]): Set<string> {
export function getPubkeysFromUserLists(
userLists: UserListEvent[],
): Set<string> {
const pubkeys = new Set<string>();
userLists.forEach(list => {
list.pubkeys.forEach(pubkey => {
userLists.forEach((list) => {
list.pubkeys.forEach((pubkey) => {
pubkeys.add(pubkey);
});
});
@ -163,12 +172,15 @@ export function getPubkeysFromUserLists(userLists: UserListEvent[]): Set<string> @@ -163,12 +172,15 @@ export function getPubkeysFromUserLists(userLists: UserListEvent[]): Set<string>
* @param kind - The list kind to filter by
* @returns Set of unique pubkeys from the specified list kind
*/
export function getPubkeysFromListKind(userLists: UserListEvent[], kind: number): Set<string> {
export function getPubkeysFromListKind(
userLists: UserListEvent[],
kind: number,
): Set<string> {
const pubkeys = new Set<string>();
userLists.forEach(list => {
userLists.forEach((list) => {
if (list.kind === kind) {
list.pubkeys.forEach(pubkey => {
list.pubkeys.forEach((pubkey) => {
pubkeys.add(pubkey);
});
}
@ -183,11 +195,22 @@ export function getPubkeysFromListKind(userLists: UserListEvent[], kind: number) @@ -183,11 +195,22 @@ export function getPubkeysFromListKind(userLists: UserListEvent[], kind: number)
* @param userLists - Array of UserListEvent objects
* @returns True if the pubkey is in any list
*/
export function isPubkeyInUserLists(pubkey: string, userLists: UserListEvent[]): boolean {
const result = userLists.some(list => list.pubkeys.includes(pubkey));
console.log(`isPubkeyInUserLists: Checking ${pubkey} against ${userLists.length} lists, result: ${result}`);
export function isPubkeyInUserLists(
pubkey: string,
userLists: UserListEvent[],
): boolean {
const result = userLists.some((list) => list.pubkeys.includes(pubkey));
console.log(
`isPubkeyInUserLists: Checking ${pubkey} against ${userLists.length} lists, result: ${result}`,
);
if (result) {
console.log(`isPubkeyInUserLists: Found ${pubkey} in lists:`, userLists.filter(list => list.pubkeys.includes(pubkey)).map(list => ({ kind: list.kind, name: list.listName })));
console.log(
`isPubkeyInUserLists: Found ${pubkey} in lists:`,
userLists.filter((list) => list.pubkeys.includes(pubkey)).map((list) => ({
kind: list.kind,
name: list.listName,
})),
);
}
return result;
}
@ -198,10 +221,13 @@ export function isPubkeyInUserLists(pubkey: string, userLists: UserListEvent[]): @@ -198,10 +221,13 @@ export function isPubkeyInUserLists(pubkey: string, userLists: UserListEvent[]):
* @param userLists - Array of UserListEvent objects
* @returns Array of list kinds that contain the pubkey
*/
export function getListKindsForPubkey(pubkey: string, userLists: UserListEvent[]): number[] {
export function getListKindsForPubkey(
pubkey: string,
userLists: UserListEvent[],
): number[] {
return userLists
.filter(list => list.pubkeys.includes(pubkey))
.map(list => list.kind);
.filter((list) => list.pubkeys.includes(pubkey))
.map((list) => list.kind);
}
/**
@ -209,7 +235,10 @@ export function getListKindsForPubkey(pubkey: string, userLists: UserListEvent[] @@ -209,7 +235,10 @@ export function getListKindsForPubkey(pubkey: string, userLists: UserListEvent[]
* This ensures follows are always cached and prioritized
* @param pubkeys - Array of pubkeys to cache profiles for
*/
export async function updateProfileCacheForPubkeys(pubkeys: string[], ndk?: NDK): Promise<void> {
export async function updateProfileCacheForPubkeys(
pubkeys: string[],
ndk?: NDK,
): Promise<void> {
if (pubkeys.length === 0) return;
try {

1
src/routes/publication/[type]/[identifier]/+layout.server.ts

@ -1,6 +1,5 @@ @@ -1,6 +1,5 @@
import type { LayoutServerLoad } from "./$types";
export const load: LayoutServerLoad = ({ url }: { url: URL }) => {
const currentUrl = `${url.origin}${url.pathname}`;

3
src/routes/publication/[type]/[identifier]/+page.ts

@ -49,7 +49,8 @@ export const load: PageLoad = async ( @@ -49,7 +49,8 @@ export const load: PageLoad = async (
// AI-NOTE: Return null for indexEvent during SSR or when fetch fails
// The component will handle client-side loading and error states
const publicationType = indexEvent?.tags.find((tag) => tag[0] === "type")?.[1] ?? "";
const publicationType =
indexEvent?.tags.find((tag) => tag[0] === "type")?.[1] ?? "";
const result = {
publicationType,

8
src/styles/notifications.css

@ -107,15 +107,11 @@ @@ -107,15 +107,11 @@
}
.message-container:hover {
box-shadow:
0 4px 6px -1px rgb(0 0 0 / 0.1),
0 2px 4px -2px rgb(0 0 0 / 0.1);
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
}
.dark .message-container:hover {
box-shadow:
0 4px 6px -1px rgb(0 0 0 / 0.3),
0 2px 4px -2px rgb(0 0 0 / 0.2);
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.3), 0 2px 4px -2px rgb(0 0 0 / 0.2);
}
/* Filter indicator styling */

3
src/styles/scrollbar.css

@ -1,7 +1,8 @@ @@ -1,7 +1,8 @@
@layer components {
/* Global scrollbar styles */
* {
scrollbar-color: rgba(87, 66, 41, 0.8) transparent; /* Transparent track, default scrollbar thumb */
scrollbar-color: rgba(87, 66, 41, 0.8)
transparent; /* Transparent track, default scrollbar thumb */
}
/* Webkit Browsers (Chrome, Safari, Edge) */

71
tests/unit/mathProcessing.test.ts

@ -3,29 +3,38 @@ import { parseAdvancedmarkup } from "../../src/lib/utils/markup/advancedMarkupPa @@ -3,29 +3,38 @@ import { parseAdvancedmarkup } from "../../src/lib/utils/markup/advancedMarkupPa
describe("Math Processing in Advanced Markup Parser", () => {
it("should process inline math inside code blocks", async () => {
const input = "Here is some inline math: `$x^2 + y^2 = z^2$` in a sentence.";
const input =
"Here is some inline math: `$x^2 + y^2 = z^2$` in a sentence.";
const result = await parseAdvancedmarkup(input);
expect(result).toContain('<span class="math-inline">\\(x^2 + y^2 = z^2\\)</span>');
expect(result).toContain(
'<span class="math-inline">\\(x^2 + y^2 = z^2\\)</span>',
);
expect(result).toContain("Here is some inline math:");
expect(result).toContain("in a sentence.");
});
it("should process display math inside code blocks", async () => {
const input = "Here is a display equation:\n\n`$$\n\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}\n$$`\n\nThis is after the equation.";
const input =
"Here is a display equation:\n\n`$$\n\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}\n$$`\n\nThis is after the equation.";
const result = await parseAdvancedmarkup(input);
expect(result).toContain('<span class="math-display">\\[\n\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}\n\\]</span>');
expect(result).toContain(
'<span class="math-display">\\[\n\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}\n\\]</span>',
);
expect(result).toContain('<p class="my-4">Here is a display equation:</p>');
expect(result).toContain('<p class="my-4">This is after the equation.</p>');
});
it("should process both inline and display math in the same code block", async () => {
const input = "Mixed math: `$\\alpha$ and $$\\beta = \\frac{1}{2}$$` in one block.";
const input =
"Mixed math: `$\\alpha$ and $$\\beta = \\frac{1}{2}$$` in one block.";
const result = await parseAdvancedmarkup(input);
expect(result).toContain('<span class="math-inline">\\(\\alpha\\)</span>');
expect(result).toContain('<span class="math-display">\\[\\beta = \\frac{1}{2}\\]</span>');
expect(result).toContain(
'<span class="math-display">\\[\\beta = \\frac{1}{2}\\]</span>',
);
expect(result).toContain("Mixed math:");
expect(result).toContain("in one block.");
});
@ -40,16 +49,20 @@ describe("Math Processing in Advanced Markup Parser", () => { @@ -40,16 +49,20 @@ describe("Math Processing in Advanced Markup Parser", () => {
});
it("should NOT process display math outside of code blocks", async () => {
const input = "This display math $$\n\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}\n$$ should not be processed.";
const input =
"This display math $$\n\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}\n$$ should not be processed.";
const result = await parseAdvancedmarkup(input);
expect(result).toContain("$$\n\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}\n$$");
expect(result).toContain(
"$$\n\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}\n$$",
);
expect(result).not.toContain('<span class="math-inline">');
expect(result).not.toContain('<span class="math-display">');
});
it("should handle code blocks without math normally", async () => {
const input = "Here is some code: `console.log('hello world')` that should not be processed.";
const input =
"Here is some code: `console.log('hello world')` that should not be processed.";
const result = await parseAdvancedmarkup(input);
expect(result).toContain("`console.log('hello world')`");
@ -58,7 +71,8 @@ describe("Math Processing in Advanced Markup Parser", () => { @@ -58,7 +71,8 @@ describe("Math Processing in Advanced Markup Parser", () => {
});
it("should handle complex math expressions with nested structures", async () => {
const input = "Complex math: `$$\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix} \\cdot \\begin{pmatrix} x \\\\ y \\end{pmatrix} = \\begin{pmatrix} ax + by \\\\ cx + dy \\end{pmatrix}$$`";
const input =
"Complex math: `$$\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix} \\cdot \\begin{pmatrix} x \\\\ y \\end{pmatrix} = \\begin{pmatrix} ax + by \\\\ cx + dy \\end{pmatrix}$$`";
const result = await parseAdvancedmarkup(input);
expect(result).toContain('<span class="math-display">');
@ -68,23 +82,32 @@ describe("Math Processing in Advanced Markup Parser", () => { @@ -68,23 +82,32 @@ describe("Math Processing in Advanced Markup Parser", () => {
});
it("should handle inline math with special characters", async () => {
const input = "Special chars: `$\\alpha, \\beta, \\gamma, \\delta$` and `$\\sum_{i=1}^{n} x_i$`";
const input =
"Special chars: `$\\alpha, \\beta, \\gamma, \\delta$` and `$\\sum_{i=1}^{n} x_i$`";
const result = await parseAdvancedmarkup(input);
expect(result).toContain('<span class="math-inline">\\(\\alpha, \\beta, \\gamma, \\delta\\)</span>');
expect(result).toContain('<span class="math-inline">\\(\\sum_{i=1}^{n} x_i\\)</span>');
expect(result).toContain(
'<span class="math-inline">\\(\\alpha, \\beta, \\gamma, \\delta\\)</span>',
);
expect(result).toContain(
'<span class="math-inline">\\(\\sum_{i=1}^{n} x_i\\)</span>',
);
});
it("should handle multiple math expressions in separate code blocks", async () => {
const input = "First: `$E = mc^2$` and second: `$$F = G\\frac{m_1 m_2}{r^2}$$`";
const input =
"First: `$E = mc^2$` and second: `$$F = G\\frac{m_1 m_2}{r^2}$$`";
const result = await parseAdvancedmarkup(input);
expect(result).toContain('<span class="math-inline">\\(E = mc^2\\)</span>');
expect(result).toContain('<span class="math-display">\\[F = G\\frac{m_1 m_2}{r^2}\\]</span>');
expect(result).toContain(
'<span class="math-display">\\[F = G\\frac{m_1 m_2}{r^2}\\]</span>',
);
});
it("should handle math expressions with line breaks in display mode", async () => {
const input = "Multi-line: `$$\n\\begin{align}\nx &= a + b \\\\\ny &= c + d\n\\end{align}\n$$`";
const input =
"Multi-line: `$$\n\\begin{align}\nx &= a + b \\\\\ny &= c + d\n\\end{align}\n$$`";
const result = await parseAdvancedmarkup(input);
expect(result).toContain('<span class="math-display">');
@ -124,7 +147,9 @@ And more regular text.`; @@ -124,7 +147,9 @@ And more regular text.`;
expect(result).toContain("`console.log('hello')`");
// Should process math
expect(result).toContain('<span class="math-inline">\\(\\pi \\approx 3.14159\\)</span>');
expect(result).toContain(
'<span class="math-inline">\\(\\pi \\approx 3.14159\\)</span>',
);
expect(result).toContain('<span class="math-display">');
expect(result).toContain("\\int_0^1 x^2 dx = \\frac{1}{3}");
});
@ -138,7 +163,8 @@ And more regular text.`; @@ -138,7 +163,8 @@ And more regular text.`;
});
it("should handle display math with dollar signs in the content", async () => {
const input = "Price display: `$$\n\\text{Total} = \\$19.99 + \\$5.99 = \\$25.98\n$$`";
const input =
"Price display: `$$\n\\text{Total} = \\$19.99 + \\$5.99 = \\$25.98\n$$`";
const result = await parseAdvancedmarkup(input);
expect(result).toContain('<span class="math-display">');
@ -156,7 +182,8 @@ And more regular text.`; @@ -156,7 +182,8 @@ And more regular text.`;
it("should handle JSON content with escaped display math", async () => {
// Simulate content from JSON where backslashes are escaped
const jsonContent = "Display math from JSON: `$$\\\\int_0^1 x^2 dx = \\\\frac{1}{3}$$`";
const jsonContent =
"Display math from JSON: `$$\\\\int_0^1 x^2 dx = \\\\frac{1}{3}$$`";
const result = await parseAdvancedmarkup(jsonContent);
expect(result).toContain('<span class="math-display">');
@ -165,7 +192,8 @@ And more regular text.`; @@ -165,7 +192,8 @@ And more regular text.`;
it("should handle JSON content with escaped dollar signs", async () => {
// Simulate content from JSON where dollar signs are escaped
const jsonContent = "Price math from JSON: `$\\\\text{Price} = \\\\\\$19.99$`";
const jsonContent =
"Price math from JSON: `$\\\\text{Price} = \\\\\\$19.99$`";
const result = await parseAdvancedmarkup(jsonContent);
expect(result).toContain('<span class="math-inline">');
@ -174,7 +202,8 @@ And more regular text.`; @@ -174,7 +202,8 @@ And more regular text.`;
it("should handle complex JSON content with multiple escaped characters", async () => {
// Simulate complex content from JSON
const jsonContent = "Complex JSON math: `$$\\\\begin{pmatrix} a & b \\\\\\\\ c & d \\\\end{pmatrix} \\\\cdot \\\\begin{pmatrix} x \\\\\\\\ y \\\\end{pmatrix}$$`";
const jsonContent =
"Complex JSON math: `$$\\\\begin{pmatrix} a & b \\\\\\\\ c & d \\\\end{pmatrix} \\\\cdot \\\\begin{pmatrix} x \\\\\\\\ y \\\\end{pmatrix}$$`";
const result = await parseAdvancedmarkup(jsonContent);
expect(result).toContain('<span class="math-display">');

8
tests/unit/tagExpansion.test.ts

@ -335,7 +335,9 @@ describe("Tag Expansion Tests", () => { @@ -335,7 +335,9 @@ describe("Tag Expansion Tests", () => {
);
// Should not include events without tags
expect(result.publications.map((p: any) => p.id)).not.toContain("no-tags");
expect(result.publications.map((p: any) => p.id)).not.toContain(
"no-tags",
);
});
});
@ -512,7 +514,9 @@ describe("Tag Expansion Tests", () => { @@ -512,7 +514,9 @@ describe("Tag Expansion Tests", () => {
// Should handle d-tags with colons correctly
expect(result.publications).toHaveLength(3);
expect(result.contentEvents.map((c: any) => c.id)).toContain("colon-content");
expect(result.contentEvents.map((c: any) => c.id)).toContain(
"colon-content",
);
});
});
});

Loading…
Cancel
Save