feat: resolve live Nostr references in blog posts and add embeds
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>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
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);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user