import { nip19 } from "nostr-tools"; // Matches NIP-27 `nostr:` references (and bare bech32 entities) embedded in // article text: npub/nprofile (mentions) and note/nevent/naddr (notes). const NOSTR_RE = /(?:nostr:)?((?:npub|nprofile|note|nevent|naddr)1[023456789acdefghjklmnpqrstuvwxyz]+)/gi; function isValidEntity(bech32: string): boolean { try { const { type } = nip19.decode(bech32); return ["npub", "nprofile", "note", "nevent", "naddr"].includes(type); } catch { return false; } } // Splits a plain-text value into text + `link` nodes, where each link's url is // `nostr:`. The markdown `a` renderer detects that scheme and swaps in // the live profile/note component. function splitText(value: string): any[] { const out: any[] = []; let last = 0; NOSTR_RE.lastIndex = 0; let m: RegExpExecArray | null; while ((m = NOSTR_RE.exec(value)) !== null) { const bech32 = m[1]; if (!isValidEntity(bech32)) continue; if (m.index > last) { out.push({ type: "text", value: value.slice(last, m.index) }); } out.push({ type: "link", url: `nostr:${bech32}`, children: [{ type: "text", value: m[0] }], }); last = m.index + m[0].length; } if (out.length === 0) return [{ type: "text", value }]; if (last < value.length) out.push({ type: "text", value: value.slice(last) }); return out; } // Remark plugin: walk the mdast tree and replace `nostr:` tokens inside text // nodes. Skips code and existing links so identifiers there are left untouched. export function remarkNostr() { return (tree: any) => { const walk = (node: any) => { if (!node || !Array.isArray(node.children)) return; const next: any[] = []; for (const child of node.children) { if (child.type === "text") { next.push(...splitText(child.value)); continue; } if (child.type !== "link" && child.type !== "inlineCode" && child.type !== "code") { walk(child); } next.push(child); } node.children = next; }; walk(tree); }; }