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.
82 lines
2.8 KiB
82 lines
2.8 KiB
import type { Event } from 'nostr-tools' |
|
|
|
/** |
|
* NIP-84 / Web Annotation style `textquoteselector` (prefix + exact + suffix). |
|
* `exact` is always {@link Event.content}; prefix/suffix are adjacent source text. |
|
* |
|
* Common shapes: |
|
* - `["textquoteselector", prefix, suffix]` (3 items) |
|
* - `["textquoteselector", "-", prefix, suffix]` — leading "-" = empty slot (Hypothesis-style) |
|
*/ |
|
function parseTextQuoteSelectorParts(tag: readonly string[]): { prefix: string; suffix: string } { |
|
if (tag.length < 2 || tag[0] !== 'textquoteselector') { |
|
return { prefix: '', suffix: '' } |
|
} |
|
if (tag.length >= 4 && tag[1] === '-') { |
|
return { |
|
prefix: (tag[2] ?? '').trim(), |
|
suffix: (tag[3] ?? '').trim() |
|
} |
|
} |
|
if (tag.length >= 3) { |
|
return { |
|
prefix: (tag[1] ?? '').trim(), |
|
suffix: (tag[2] ?? '').trim() |
|
} |
|
} |
|
return { prefix: '', suffix: '' } |
|
} |
|
|
|
/** `["textpositionselector", start, end]` — character offsets into a full document string. */ |
|
function parseTextPositionSelector(tag: readonly string[]): { start: number; end: number } | null { |
|
if (tag.length < 3 || tag[0] !== 'textpositionselector') return null |
|
const start = parseInt(tag[1] ?? '', 10) |
|
const end = parseInt(tag[2] ?? '', 10) |
|
if (Number.isNaN(start) || Number.isNaN(end) || start < 0 || end <= start) return null |
|
return { start, end } |
|
} |
|
|
|
export type Nip84HighlightDisplay = { |
|
/** Full passage to show in the quote box */ |
|
fullText: string |
|
/** Substring of fullText to wrap in <mark> */ |
|
markedSpan: string |
|
} |
|
|
|
/** |
|
* Resolve which span to mark inside which full text, using `context`, `textquoteselector`, |
|
* and optionally `textpositionselector` (only when offsets fit the base string). |
|
*/ |
|
export function resolveNip84HighlightDisplay(event: Pick<Event, 'content' | 'tags'>): Nip84HighlightDisplay { |
|
const highlightedText = event.content ?? '' |
|
const tags = event.tags |
|
|
|
const contextTag = tags.find((t) => t[0] === 'context') |
|
const contextBody = contextTag?.[1]?.trim() ? contextTag[1] : undefined |
|
|
|
const posTag = tags.find((t) => t[0] === 'textpositionselector') |
|
const pos = posTag ? parseTextPositionSelector(posTag) : null |
|
|
|
if (contextBody && pos) { |
|
const { start, end } = pos |
|
if (end <= contextBody.length) { |
|
const slice = contextBody.slice(start, end) |
|
if (slice.length > 0) { |
|
return { fullText: contextBody, markedSpan: slice } |
|
} |
|
} |
|
} |
|
|
|
if (contextBody) { |
|
return { fullText: contextBody, markedSpan: highlightedText } |
|
} |
|
|
|
const tqs = tags.find((t) => t[0] === 'textquoteselector') |
|
if (tqs) { |
|
const { prefix, suffix } = parseTextQuoteSelectorParts(tqs) |
|
const fullText = `${prefix}${highlightedText}${suffix}` |
|
return { fullText, markedSpan: highlightedText } |
|
} |
|
|
|
return { fullText: highlightedText, markedSpan: highlightedText } |
|
}
|
|
|