SRC-04: such-oberflaeche-mit-hervorhebung
Such-Oberfläche (Next.js/React/TS) mit Live-Trefferliste und Hervorhebung der Suchbegriffe im Kontext (Snippet), auf web/shl (SHL-01) aufbauend. - app/api/search/route.ts: schlanke Backend-for-Frontend-Route gegen dieselbe Manticore-Instanz wie mail/internal/search (SRC-01/SRC-03), fordert Highlights mit eigenen Markern statt HTML an. - lib/highlight.ts: zerlegt markierten Snippet-Text in reine Textsegmente, kein dangerouslySetInnerHTML — Mailinhalte werden nie als HTML interpretiert. - app/page.tsx: Sucheingabe, Trefferliste mit <mark>-Hervorhebung, verständlicher Hinweis bei leerem Ergebnis. - app/mail/[messageId]/page.tsx: öffnet mit Anker #fundstelle und hervorgehobenem Snippet (voller Mail-Inhaltsabruf folgt mit INT-01). - lib/contrast.ts: reale WCAG-2.1-Kontrastberechnung. Prüfungen (alle real durchgeführt, siehe mail/docs/SRC-04-PRUEFPROTOKOLL.md): 1. Manueller Test gegen echten next start + live Manticore auf 192.168.1.131: Hervorhebung real bestätigt. 2. lib/highlightColors.test.ts: echte WCAG-Berechnung, Hell 14,29:1, Dunkel 6,43:1 (>= 4.5:1 AA). 3. Sonderzeichen-Anfrage real gegen laufenden Server: 200 OK, kein Absturz; zusätzlich automatisiert gegen Skript-Tags/Unicode. Kein Umbau: mail/internal/*, web/shl, web/retention-admin unverändert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
9748307f12
commit
d23438d3d0
@@ -0,0 +1,44 @@
|
||||
// SRC-04: zerlegt einen von der Mail-Suche gelieferten Snippet-Text in
|
||||
// Textsegmente. Manticore markiert Treffer serverseitig mit den Markern
|
||||
// HIGHLIGHT_BEFORE/HIGHLIGHT_AFTER (siehe app/api/search/route.ts) — bewusst
|
||||
// KEIN HTML von Manticore übernehmen (Mailinhalte sind nicht vertrauenswürdig,
|
||||
// könnten selbst Markup enthalten). splitHighlighted liefert reine
|
||||
// Textsegmente, die die aufrufende Komponente als Textknoten rendert
|
||||
// (kein dangerouslySetInnerHTML nötig, damit keine XSS-Lücke möglich).
|
||||
|
||||
export const HIGHLIGHT_BEFORE = "⦃⦃";
|
||||
export const HIGHLIGHT_AFTER = "⦄⦄";
|
||||
|
||||
export interface HighlightSegment {
|
||||
text: string;
|
||||
matched: boolean;
|
||||
}
|
||||
|
||||
export function splitHighlighted(snippet: string): HighlightSegment[] {
|
||||
const segments: HighlightSegment[] = [];
|
||||
let rest = snippet;
|
||||
|
||||
while (rest.length > 0) {
|
||||
const startIdx = rest.indexOf(HIGHLIGHT_BEFORE);
|
||||
if (startIdx === -1) {
|
||||
segments.push({ text: rest, matched: false });
|
||||
break;
|
||||
}
|
||||
if (startIdx > 0) {
|
||||
segments.push({ text: rest.slice(0, startIdx), matched: false });
|
||||
}
|
||||
const afterStart = rest.slice(startIdx + HIGHLIGHT_BEFORE.length);
|
||||
const endIdx = afterStart.indexOf(HIGHLIGHT_AFTER);
|
||||
if (endIdx === -1) {
|
||||
// Unvollständiges Markerpaar (sollte bei korrekter Manticore-Antwort
|
||||
// nicht vorkommen) — Rest als unmarkierten Text behandeln, statt die
|
||||
// Anzeige abstürzen zu lassen (Pflichtprüfung 3).
|
||||
segments.push({ text: afterStart, matched: false });
|
||||
break;
|
||||
}
|
||||
segments.push({ text: afterStart.slice(0, endIdx), matched: true });
|
||||
rest = afterStart.slice(endIdx + HIGHLIGHT_AFTER.length);
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
Reference in New Issue
Block a user