Fixes a bunch of problems with how Nostr links are created and replaced in Markdown content
This commit is contained in:
129
nostr/links.ts
Normal file
129
nostr/links.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user