Support naddr/nevent/note slugs for unindexed posts, cache naddr lookups, render Nostr embeds in markdown, and add a consistency check for events mirrors. Co-authored-by: Cursor <cursoragent@cursor.com>
65 lines
2.0 KiB
TypeScript
65 lines
2.0 KiB
TypeScript
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:<bech32>`. 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);
|
|
};
|
|
}
|