Something went wrong. Try again.
Original-language reader for Kant, Aristotle & more: select any passage to gloss, translate, or hear it read aloud, with exact citations.
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061import type { DocumentModel } from "@/lib/schemas/model"
/** * Selection normalization (DATA-ARCHITECTURE.md §1.5) — the shared primitive * behind Gloss, Translate, and Read aloud (§9). NOT used by Copy-with-citation * or Correction, which act on the verbatim selection. * * 1. snap both endpoints OUTWARD to whole tokens (§1.3) * 2. the surviving span is `text.slice(firstToken.start, lastToken.end)` * (token boundaries already exclude surrounding whitespace/punctuation) * 3. empty if no token survives → caller offers none of the three actions * * So *einsfra* inside *Seinsfrage*, or *Seinsfrage,* with a trailing comma, * all normalize to the same canonical quote `Seinsfrage` — which is what makes * the `hash(quote + citation)` cache key collapse. */export interface NormalizedSpan { start: number end: number quote: string}
export function normalizeSelection( model: DocumentModel, selStart: number, selEnd: number,): NormalizedSpan | null { const lo = Math.min(selStart, selEnd) const hi = Math.max(selStart, selEnd) const caret = lo === hi
let firstStart = Number.POSITIVE_INFINITY let lastEnd = -1
for (const t of model.tokens) { if (t.start >= hi && !caret) break // tokens are sorted; nothing further can intersect const intersects = t.start < hi && t.end > lo const caretInside = caret && t.start <= lo && lo < t.end if (intersects || caretInside) { if (t.start < firstStart) firstStart = t.start if (t.end > lastEnd) lastEnd = t.end } if (caret && t.start > lo) break }
if (lastEnd < 0) return null const quote = model.text.slice(firstStart, lastEnd) if (quote.length === 0) return null return { start: firstStart, end: lastEnd, quote }}
/** Count word tokens fully inside `[start, end)` — 1 ⇒ a single-word span (§5+). */export function spanTokenCount(model: DocumentModel, start: number, end: number): number { let n = 0 for (const t of model.tokens) { if (t.start >= end) break if (t.start >= start && t.end <= end) n++ } return n}