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
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die README; an keiner Stelle Bedeutung, nur Zeichensetzung.
66 lines
2.1 KiB
TypeScript
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
|
|
}
|