You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
75 lines
2.6 KiB
75 lines
2.6 KiB
import { Fragment, type ReactNode } from 'react' |
|
import { kinds, type Event } from 'nostr-tools' |
|
import { resolveNip84HighlightDisplay } from '@/lib/nip84-highlight-display' |
|
|
|
/** Same classes as {@link Highlight} NIP-84 span marks (keep in sync manually). */ |
|
export const NIP84_HIGHLIGHT_MARK_CLASSNAME = |
|
'bg-green-200 dark:bg-green-600 dark:text-white px-1 rounded font-medium' |
|
|
|
function stripOuterQuotes(s: string): string { |
|
let t = s.trim() |
|
if (t.startsWith('"') && t.endsWith('"')) { |
|
t = t.slice(1, -1).trim() |
|
} |
|
return t |
|
} |
|
|
|
function highlightTargetsNoteHex(h: Event, opHex: string): boolean { |
|
const want = opHex.trim().toLowerCase() |
|
if (!/^[0-9a-f]{64}$/i.test(want)) return false |
|
return h.tags.some( |
|
(t) => (t[0] === 'e' || t[0] === 'E') && typeof t[1] === 'string' && t[1].trim().toLowerCase() === want |
|
) |
|
} |
|
|
|
/** Non-overlapping merged intervals for first occurrence of each highlight’s marked span in `baseText`. */ |
|
export function mergeNip84MarkedIntervals( |
|
baseText: string, |
|
highlightEvents: Event[], |
|
opEventHexId: string |
|
): { start: number; end: number }[] { |
|
const intervals: { start: number; end: number }[] = [] |
|
for (const ev of highlightEvents) { |
|
if (ev.kind !== kinds.Highlights) continue |
|
if (!highlightTargetsNoteHex(ev, opEventHexId)) continue |
|
const { markedSpan } = resolveNip84HighlightDisplay(ev) |
|
const needle = stripOuterQuotes(markedSpan) |
|
if (!needle) continue |
|
const idx = baseText.indexOf(needle) |
|
if (idx >= 0) intervals.push({ start: idx, end: idx + needle.length }) |
|
} |
|
if (intervals.length === 0) return [] |
|
intervals.sort((a, b) => a.start - b.start) |
|
const merged: { start: number; end: number }[] = [] |
|
for (const cur of intervals) { |
|
const prev = merged[merged.length - 1] |
|
if (!prev || cur.start > prev.end) merged.push({ ...cur }) |
|
else prev.end = Math.max(prev.end, cur.end) |
|
} |
|
return merged |
|
} |
|
|
|
export function renderPlaintextWithNip84MergedMarks( |
|
baseText: string, |
|
merged: { start: number; end: number }[] |
|
): ReactNode { |
|
if (merged.length === 0) return baseText |
|
const nodes: ReactNode[] = [] |
|
let cursor = 0 |
|
let i = 0 |
|
for (const m of merged) { |
|
if (cursor < m.start) nodes.push(<Fragment key={`t-${i++}`}>{baseText.slice(cursor, m.start)}</Fragment>) |
|
nodes.push( |
|
<mark |
|
key={`m-${m.start}-${m.end}`} |
|
className={NIP84_HIGHLIGHT_MARK_CLASSNAME} |
|
data-nip84-highlight="span" |
|
> |
|
{baseText.slice(m.start, m.end)} |
|
</mark> |
|
) |
|
cursor = m.end |
|
} |
|
if (cursor < baseText.length) nodes.push(<Fragment key={`t-${i++}`}>{baseText.slice(cursor)}</Fragment>) |
|
return <>{nodes}</> |
|
}
|
|
|