Files
AfiS-Aktensystem/web/lib/chat/mentions.ts
T
Harlekin e0b03ea336
Prüfung / Lint, Typen, Tests (push) Canceled after 0s
Prüfung / Images bauen (pdf) (push) Canceled after 0s
Prüfung / Images bauen (web) (push) Canceled after 0s
Prüfung / Images bauen (worker) (push) Canceled after 0s
Images / bauen (pdf, docker/pdf.Dockerfile) (push) Canceled after 0s
Images / bauen (web, docker/web.Dockerfile) (push) Canceled after 0s
Images / bauen (worker, docker/worker.Dockerfile) (push) Canceled after 0s
Gedankenstriche durch Bindestriche ersetzt
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die
README; an keiner Stelle Bedeutung, nur Zeichensetzung.
2026-09-04 08:54:52 +02:00

66 lines
2.1 KiB
TypeScript

/**
* 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<string, string> = 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
}