mirror of
https://github.com/dergigi/boris.git
synced 2026-02-10 17:44:34 +01:00
feat: resolve NIP-19 identifiers in article content
- Add nostrUriResolver utility to detect and replace nostr: URIs - Support npub, note, nprofile, nevent, and naddr identifiers - Convert nostr: URIs to clickable njump.me links - Process markdown before rendering to handle nostr mentions - Add CSS styling for nostr-uri-link class - Implements NIP-19 and NIP-27 (nostr: URI scheme) Nostr-native articles can now contain references like: - nostr:npub1... → @npub1abc... - nostr:note1... → note:note1abc... - nostr:naddr1... → article:identifier All identifiers become clickable links to njump.me
This commit is contained in:
134
src/utils/nostrUriResolver.tsx
Normal file
134
src/utils/nostrUriResolver.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import React from 'react'
|
||||
import { decode, npubEncode, noteEncode } from 'nostr-tools/nip19'
|
||||
import { DecodeResult } from 'nostr-tools/nip19'
|
||||
|
||||
/**
|
||||
* Regular expression to match nostr: URIs and bare NIP-19 identifiers
|
||||
* Matches: nostr:npub1..., nostr:note1..., nostr:nprofile1..., nostr:nevent1..., nostr:naddr1...
|
||||
* Also matches bare identifiers without the nostr: prefix
|
||||
*/
|
||||
const NOSTR_URI_REGEX = /(?:nostr:)?((npub|note|nprofile|nevent|naddr)1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{58,})/gi
|
||||
|
||||
/**
|
||||
* Extract all nostr URIs from text
|
||||
*/
|
||||
export function extractNostrUris(text: string): string[] {
|
||||
const matches = text.match(NOSTR_URI_REGEX)
|
||||
if (!matches) return []
|
||||
|
||||
// Extract just the NIP-19 identifier (without nostr: prefix)
|
||||
return matches.map(match => {
|
||||
const cleanMatch = match.replace(/^nostr:/, '')
|
||||
return cleanMatch
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a NIP-19 identifier and return a human-readable link
|
||||
*/
|
||||
export function createNostrLink(encoded: string): string {
|
||||
try {
|
||||
const decoded = decode(encoded)
|
||||
|
||||
switch (decoded.type) {
|
||||
case 'npub':
|
||||
return `https://njump.me/${encoded}`
|
||||
case 'nprofile':
|
||||
return `https://njump.me/${encoded}`
|
||||
case 'note':
|
||||
return `https://njump.me/${encoded}`
|
||||
case 'nevent':
|
||||
return `https://njump.me/${encoded}`
|
||||
case 'naddr':
|
||||
return `https://njump.me/${encoded}`
|
||||
default:
|
||||
return `https://njump.me/${encoded}`
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to decode nostr URI:', encoded, error)
|
||||
return `https://njump.me/${encoded}`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a display label for a nostr URI
|
||||
*/
|
||||
export function getNostrUriLabel(encoded: string): string {
|
||||
try {
|
||||
const decoded = decode(encoded)
|
||||
|
||||
switch (decoded.type) {
|
||||
case 'npub':
|
||||
return `@${encoded.slice(0, 12)}...`
|
||||
case 'nprofile':
|
||||
const npub = npubEncode(decoded.data.pubkey)
|
||||
return `@${npub.slice(0, 12)}...`
|
||||
case 'note':
|
||||
return `note:${encoded.slice(5, 12)}...`
|
||||
case 'nevent':
|
||||
const note = noteEncode(decoded.data.id)
|
||||
return `note:${note.slice(5, 12)}...`
|
||||
case 'naddr':
|
||||
return `article:${decoded.data.identifier || 'untitled'}`
|
||||
default:
|
||||
return encoded.slice(0, 16) + '...'
|
||||
}
|
||||
} catch (error) {
|
||||
return encoded.slice(0, 16) + '...'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace nostr: URIs in markdown with proper markdown links
|
||||
* This converts: nostr:npub1... to [label](link)
|
||||
*/
|
||||
export function replaceNostrUrisInMarkdown(markdown: string): string {
|
||||
return markdown.replace(NOSTR_URI_REGEX, (match) => {
|
||||
// Extract just the NIP-19 identifier (without nostr: prefix)
|
||||
const encoded = match.replace(/^nostr:/, '')
|
||||
const link = createNostrLink(encoded)
|
||||
const label = getNostrUriLabel(encoded)
|
||||
|
||||
return `[${label}](${link})`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace nostr: URIs in HTML with clickable links
|
||||
* This is used when processing HTML content directly
|
||||
*/
|
||||
export function replaceNostrUrisInHTML(html: string): string {
|
||||
return html.replace(NOSTR_URI_REGEX, (match) => {
|
||||
// Extract just the NIP-19 identifier (without nostr: prefix)
|
||||
const encoded = match.replace(/^nostr:/, '')
|
||||
const link = createNostrLink(encoded)
|
||||
const label = getNostrUriLabel(encoded)
|
||||
|
||||
return `<a href="${link}" class="nostr-uri-link" target="_blank" rel="noopener noreferrer">${label}</a>`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get decoded information from a nostr URI for detailed display
|
||||
*/
|
||||
export function getNostrUriInfo(encoded: string): {
|
||||
type: string
|
||||
decoded: DecodeResult | null
|
||||
link: string
|
||||
label: string
|
||||
} {
|
||||
let decoded: DecodeResult | null = null
|
||||
try {
|
||||
decoded = decode(encoded)
|
||||
} catch (error) {
|
||||
// ignore decoding errors
|
||||
}
|
||||
|
||||
return {
|
||||
type: decoded?.type || 'unknown',
|
||||
decoded,
|
||||
link: createNostrLink(encoded),
|
||||
label: getNostrUriLabel(encoded)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user