mirror of
https://github.com/dergigi/boris.git
synced 2025-12-18 15:14:20 +01:00
Removed unused linkEnd variable and prefixed unused type parameter with underscore to satisfy linter and type checker.
247 lines
7.6 KiB
TypeScript
247 lines
7.6 KiB
TypeScript
import { decode, npubEncode, noteEncode } from 'nostr-tools/nip19'
|
|
import { getNostrUrl } from '../config/nostrGateways'
|
|
|
|
/**
|
|
* 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
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Extract all naddr (article) identifiers from text
|
|
*/
|
|
export function extractNaddrUris(text: string): string[] {
|
|
const allUris = extractNostrUris(text)
|
|
return allUris.filter(uri => {
|
|
try {
|
|
const decoded = decode(uri)
|
|
return decoded.type === 'naddr'
|
|
} catch {
|
|
return false
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Decode a NIP-19 identifier and return a human-readable link
|
|
* For articles (naddr) and profiles (npub/nprofile), returns internal app links
|
|
* For other types, returns an external gateway link
|
|
*/
|
|
export function createNostrLink(encoded: string): string {
|
|
try {
|
|
const decoded = decode(encoded)
|
|
|
|
switch (decoded.type) {
|
|
case 'naddr':
|
|
// For articles, link to our internal app route
|
|
return `/a/${encoded}`
|
|
case 'npub':
|
|
// For profiles, link to our internal app route
|
|
return `/p/${encoded}`
|
|
case 'nprofile': {
|
|
// For nprofile, convert to npub and link to our internal app route
|
|
const npub = npubEncode(decoded.data.pubkey)
|
|
return `/p/${npub}`
|
|
}
|
|
case 'note':
|
|
case 'nevent':
|
|
return getNostrUrl(encoded)
|
|
default:
|
|
return getNostrUrl(encoded)
|
|
}
|
|
} catch (error) {
|
|
console.warn('Failed to decode nostr URI:', encoded, error)
|
|
return getNostrUrl(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': {
|
|
// For articles, show the identifier if available
|
|
const identifier = decoded.data.identifier
|
|
if (identifier && identifier.length > 0) {
|
|
// Truncate long identifiers but keep them readable
|
|
return identifier.length > 40 ? `${identifier.slice(0, 37)}...` : identifier
|
|
}
|
|
return 'nostr article'
|
|
}
|
|
default:
|
|
return encoded.slice(0, 16) + '...'
|
|
}
|
|
} catch (error) {
|
|
return encoded.slice(0, 16) + '...'
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Process markdown to replace nostr URIs while skipping those inside markdown links
|
|
* This prevents nested markdown link issues when nostr identifiers appear in URLs
|
|
*/
|
|
function replaceNostrUrisSafely(
|
|
markdown: string,
|
|
getReplacement: (encoded: string) => string
|
|
): string {
|
|
// Pattern to match markdown links: [text](url)
|
|
// Uses a more robust approach to handle URLs with special characters
|
|
// Match: [ followed by any text (including escaped brackets), then ]( then URL (can contain parentheses if escaped), then )
|
|
const markdownLinkRegex = /\[(?:[^\]]|\\\])*\]\((?:[^)]|\\\))*\)/g
|
|
|
|
// Track positions where we're inside a markdown link URL
|
|
const linkRanges: Array<{ start: number, end: number }> = []
|
|
let match: RegExpExecArray | null
|
|
|
|
// Reset regex lastIndex
|
|
markdownLinkRegex.lastIndex = 0
|
|
|
|
// Find all markdown links and track their URL positions
|
|
while ((match = markdownLinkRegex.exec(markdown)) !== null) {
|
|
const linkStart = match.index
|
|
|
|
// Find the start of the URL part (after "]( )
|
|
const urlStartMatch = match[0].match(/\]\(/)
|
|
if (urlStartMatch) {
|
|
const urlStartOffset = match[0].indexOf(urlStartMatch[0]) + urlStartMatch[0].length
|
|
const urlEndOffset = match[0].length - 1 // -1 to exclude the closing )
|
|
|
|
linkRanges.push({
|
|
start: linkStart + urlStartOffset,
|
|
end: linkStart + urlEndOffset
|
|
})
|
|
}
|
|
}
|
|
|
|
// Check if a position is inside any markdown link URL
|
|
const isInsideLinkUrl = (pos: number): boolean => {
|
|
return linkRanges.some(range => pos >= range.start && pos < range.end)
|
|
}
|
|
|
|
// Replace nostr URIs, but skip those inside link URLs
|
|
// Callback params: (match, encoded, type, offset, string)
|
|
return markdown.replace(NOSTR_URI_REGEX, (match, encoded, _type, offset) => {
|
|
// Check if this match is inside a markdown link URL
|
|
if (isInsideLinkUrl(offset)) {
|
|
// Don't replace - return original match
|
|
return match
|
|
}
|
|
|
|
// encoded is already the NIP-19 identifier without nostr: prefix (from capture group)
|
|
return getReplacement(encoded)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Replace nostr: URIs in markdown with proper markdown links
|
|
* This converts: nostr:npub1... to [label](link)
|
|
*/
|
|
export function replaceNostrUrisInMarkdown(markdown: string): string {
|
|
return replaceNostrUrisSafely(markdown, (encoded) => {
|
|
const link = createNostrLink(encoded)
|
|
const label = getNostrUriLabel(encoded)
|
|
return `[${label}](${link})`
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Replace nostr: URIs in markdown with proper markdown links, using resolved titles for articles
|
|
* This converts: nostr:naddr1... to [Article Title](link)
|
|
* @param markdown The markdown content to process
|
|
* @param articleTitles Map of naddr -> title for resolved articles
|
|
*/
|
|
export function replaceNostrUrisInMarkdownWithTitles(
|
|
markdown: string,
|
|
articleTitles: Map<string, string>
|
|
): string {
|
|
return replaceNostrUrisSafely(markdown, (encoded) => {
|
|
const link = createNostrLink(encoded)
|
|
|
|
// For articles, use the resolved title if available
|
|
try {
|
|
const decoded = decode(encoded)
|
|
if (decoded.type === 'naddr' && articleTitles.has(encoded)) {
|
|
const title = articleTitles.get(encoded)!
|
|
return `[${title}](${link})`
|
|
}
|
|
} catch (error) {
|
|
// Ignore decode errors, fall through to default label
|
|
}
|
|
|
|
// For other types or if title not resolved, use default label
|
|
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: ReturnType<typeof decode> | null
|
|
link: string
|
|
label: string
|
|
} {
|
|
let decoded: ReturnType<typeof decode> | null = null
|
|
try {
|
|
decoded = decode(encoded)
|
|
} catch (error) {
|
|
// ignore decoding errors
|
|
}
|
|
|
|
return {
|
|
type: decoded?.type || 'unknown',
|
|
decoded,
|
|
link: createNostrLink(encoded),
|
|
label: getNostrUriLabel(encoded)
|
|
}
|
|
}
|
|
|