138 lines
4.1 KiB
TypeScript
138 lines
4.1 KiB
TypeScript
import { lookupUsernameByPubkey } from "../directory.ts";
|
|
import { nip19 } from "@nostr/tools";
|
|
import config from "../config.ts";
|
|
|
|
const nostrUriRegex = /(nostr:|@)(nprofile|naddr|nevent|npub)[a-z0-9]+/g;
|
|
|
|
export async function nostrUriToUrl(uri: string): Promise<string> {
|
|
const bech32 = uri.replace(/^(nostr:|@)/, "");
|
|
|
|
if (bech32.match(/^(naddr|nprofile|npub)/)) {
|
|
try {
|
|
const r = nip19.decode(bech32);
|
|
let username;
|
|
|
|
switch (r.type) {
|
|
case "naddr":
|
|
username = await lookupUsernameByPubkey(r.data.pubkey);
|
|
if (username) return `/${bech32}`;
|
|
break;
|
|
case "nprofile":
|
|
username = await lookupUsernameByPubkey(r.data.pubkey);
|
|
if (username) return `/@${username}`;
|
|
break;
|
|
case "npub":
|
|
username = await lookupUsernameByPubkey(r.data);
|
|
if (username) return `/@${username}`;
|
|
break;
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
|
|
return `${config.njump_url}/${bech32}`;
|
|
}
|
|
|
|
export async function replaceNostrUris(markdown: string): Promise<string> {
|
|
const protectedRegex =
|
|
/(`{3,}[\s\S]*?`{3,})|(`[^`]*`)|(<pre>[\s\S]*?<\/pre>)|(https?:\/\/[^\s<>"']+)/gi;
|
|
|
|
// Split text into segments: unprotected text and protected areas (code blocks, URLs)
|
|
const segments: string[] = [];
|
|
let lastIndex = 0;
|
|
|
|
markdown.replace(
|
|
protectedRegex,
|
|
(match, _fencedCode, _inlineCode, _preTag, _url, index) => {
|
|
segments.push(markdown.slice(lastIndex, index));
|
|
segments.push(match);
|
|
lastIndex = index + match.length;
|
|
return match;
|
|
},
|
|
);
|
|
segments.push(markdown.slice(lastIndex));
|
|
|
|
// Process each segment
|
|
let result = "";
|
|
for (let i = 0; i < segments.length; i++) {
|
|
if (i % 2 === 1 || protectedRegex.test(segments[i])) {
|
|
// Protected segment (code block or URL), leave unchanged
|
|
result += segments[i];
|
|
} else {
|
|
// Unprotected text, replace URIs and handle markdown links
|
|
result += await processUnprotectedText(segments[i]);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
async function processUnprotectedText(text: string): Promise<string> {
|
|
const markdownLinkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
let lastIndex = 0;
|
|
const parts: string[] = [];
|
|
|
|
// Process markdown links first
|
|
let match;
|
|
while ((match = markdownLinkRegex.exec(text)) !== null) {
|
|
const [fullMatch, linkText, target] = match;
|
|
|
|
// Add text before the link
|
|
parts.push(await replaceUrisInText(text.slice(lastIndex, match.index)));
|
|
|
|
// Process the link target
|
|
if (
|
|
nostrUriRegex.test(target) && target.match(nostrUriRegex)![0] === target
|
|
) {
|
|
// Target is a Nostr URI, replace with resolved URL
|
|
const resolvedUrl = await nostrUriToUrl(target);
|
|
parts.push(`[${linkText}](${resolvedUrl})`);
|
|
} else {
|
|
// Not a Nostr URI, keep the original link
|
|
parts.push(fullMatch);
|
|
}
|
|
|
|
lastIndex = match.index + fullMatch.length;
|
|
}
|
|
|
|
// Add any remaining text after the last link
|
|
parts.push(await replaceUrisInText(text.slice(lastIndex)));
|
|
|
|
return parts.join("");
|
|
}
|
|
|
|
async function replaceUrisInText(text: string): Promise<string> {
|
|
let modifiedText = text;
|
|
const replacements: { start: number; end: number; replacement: string }[] =
|
|
[];
|
|
|
|
// Collect all replacements for bare Nostr URIs
|
|
let match;
|
|
while ((match = nostrUriRegex.exec(modifiedText)) !== null) {
|
|
const fullUri = match[0];
|
|
const url = await nostrUriToUrl(fullUri);
|
|
const linkTitle = cleanUriForTitle(fullUri);
|
|
const markdownLink = `[${linkTitle}](${url})`;
|
|
replacements.push({
|
|
start: match.index,
|
|
end: match.index + fullUri.length,
|
|
replacement: markdownLink,
|
|
});
|
|
}
|
|
|
|
// Apply replacements from right to left to avoid index shifting
|
|
for (let i = replacements.length - 1; i >= 0; i--) {
|
|
const { start, end, replacement } = replacements[i];
|
|
modifiedText = modifiedText.slice(0, start) + replacement +
|
|
modifiedText.slice(end);
|
|
}
|
|
|
|
return modifiedText;
|
|
}
|
|
|
|
function cleanUriForTitle(uri: string): string {
|
|
// Remove "nostr:" prefix, keep "@" for the title
|
|
return uri.startsWith("nostr:") ? uri.replace(/^nostr:/, "") : uri;
|
|
}
|