/** * Erwähnungen im Nachrichtentext. * * **Ohne Abhängigkeiten**, weil die Oberfläche dieselbe Erkennung für die * Autovervollständigung braucht. Aufgelöst wird trotzdem serverseitig: der * Client schickt Text, der Server bestimmt, wer gemeint ist. */ /** `@name` - Buchstaben, Ziffern, Punkt, Unterstrich, Bindestrich. */ export const MENTION = /@([a-z0-9._-]{2,32})/gi export function extractMentionNames(body: string): string[] { return [...new Set([...body.matchAll(MENTION)].map((match) => match[1]!.toLowerCase()))] } /** * Zerlegt einen Text in Stücke, damit die Anzeige Erwähnungen und * Aktenzeichen hervorheben kann, ohne HTML aus Nutzertext zu bauen. * Siehe docs/07: kein `dangerouslySetInnerHTML` für Nutzerinhalte. */ export type Piece = | { kind: 'text'; value: string } | { kind: 'mention'; value: string; userId: string | null } | { kind: 'caseNumber'; value: string } const CASE_NUMBER = /\b[A-Za-z][A-Za-z0-9-]*-\d{4}-\d{3,8}\b/g export function splitBody( body: string, bekannt: Map = new Map(), ): Piece[] { const treffer: { start: number; end: number; piece: Piece }[] = [] for (const match of body.matchAll(MENTION)) { const name = match[1]!.toLowerCase() treffer.push({ start: match.index, end: match.index + match[0].length, piece: { kind: 'mention', value: match[0], userId: bekannt.get(name) ?? null }, }) } for (const match of body.matchAll(CASE_NUMBER)) { treffer.push({ start: match.index, end: match.index + match[0].length, piece: { kind: 'caseNumber', value: match[0] }, }) } treffer.sort((a, b) => a.start - b.start) const stuecke: Piece[] = [] let position = 0 for (const { start, end, piece } of treffer) { // Überlappungen kommen vor: `@AfiS-2026-0001` ist beides. Der erste // Treffer gewinnt, der zweite wird verworfen. if (start < position) continue if (start > position) stuecke.push({ kind: 'text', value: body.slice(position, start) }) stuecke.push(piece) position = end } if (position < body.length) stuecke.push({ kind: 'text', value: body.slice(position) }) return stuecke }