"""Rendering HTML per i 3 tab di output (Evidenziato / Anonimizzato / Report).""" import html import re as _re from presidio_analyzer import RecognizerResult from config import LABEL_IT, SEVERITY_COLORS, SEVERITY_LABELS, SEVERITY_ORDER, get_severity _BOX = ( "border:1px solid #e5e7eb; border-radius:10px; " "padding:20px 24px; background:#ffffff; " "min-height:200px; box-sizing:border-box;" ) _FONT = "Arial, Helvetica, sans-serif" _TEXT = f"font-family:{_FONT}; font-size:0.95em; line-height:1.95; white-space:pre-wrap; word-wrap:break-word; color:#111827;" _PH_RE = _re.compile(r"\[[A-Z_]+(?:_\d+)?\]") def _src(r: RecognizerResult) -> str: p = (r.recognition_metadata or {}).get("source_priority", -1) return {0: "REGEX", 1: "NER", 2: "GLINER"}.get(p, "?") def _boosted(r: RecognizerResult) -> bool: return bool((r.recognition_metadata or {}).get("post_boost", False)) def _agree(r: RecognizerResult) -> int: return int((r.recognition_metadata or {}).get("cross_layer_agreement", 0)) def _badge(r: RecognizerResult) -> str: label = LABEL_IT.get(r.entity_type, r.entity_type) return f"{label}+" if _boosted(r) else label def _box(inner: str) -> str: return f'
{inner}
' def _empty(msg: str) -> str: return _box(f'

{msg}

') # --------------------------------------------------------------------------- # Tab 1 – Testo evidenziato # --------------------------------------------------------------------------- def render_highlighted_text(text: str, entities: list[RecognizerResult]) -> str: if not text: return _empty("Inserisci del testo per iniziare.") if not entities: return _box(f'
{html.escape(text)}
') parts: list[str] = [] cursor = 0 for r in sorted(entities, key=lambda r: r.start): if r.start > cursor: parts.append(html.escape(text[cursor:r.start])) original = text[r.start:r.end] sev = get_severity(r.entity_type) c = SEVERITY_COLORS[sev] badge = _badge(r) boost = _boosted(r) an = _agree(r) tip = f"Score: {r.score:.2f} · {_src(r)}" if an >= 2: tip += f" · ⚡ {an} livelli concordano" if boost: tip += " · ✓ Doppio match regex" border_b = f"border-bottom:2px solid {c['border']};" if boost else "" parts.append( f'' f'{html.escape(original)}' f'{html.escape(badge)}' ) cursor = r.end if cursor < len(text): parts.append(html.escape(text[cursor:])) return _box(f'
{"".join(parts)}
') # --------------------------------------------------------------------------- # Tab 2 – Testo anonimizzato # --------------------------------------------------------------------------- def render_anonymized_text(anon_text: str) -> str: if not anon_text: return _empty("Nessun risultato.") def _hl(m: _re.Match) -> str: return ( f'{html.escape(m.group())}' ) return _box(f'
{_PH_RE.sub(_hl, html.escape(anon_text))}
') # --------------------------------------------------------------------------- # Tab 3 – Report categorizzato # --------------------------------------------------------------------------- def _card(sev: str, count: int) -> str: c = SEVERITY_COLORS[sev] return ( f'
' f'
Gravità {SEVERITY_LABELS[sev]}
' f'
{count}
' ) def render_categorized_report(text: str, entities: list[RecognizerResult]) -> str: if not entities: return _empty("Nessuna entità rilevata sopra lo score minimo.") by_sev: dict[str, list[RecognizerResult]] = {s: [] for s in SEVERITY_ORDER} for r in entities: by_sev[get_severity(r.entity_type)].append(r) parts: list[str] = [f'
'] parts.append('
') for sev in SEVERITY_ORDER: parts.append(_card(sev, len(by_sev[sev]))) parts.append('
') for sev in SEVERITY_ORDER: items = by_sev[sev] if not items: continue c = SEVERITY_COLORS[sev] parts.append( f'

' f'Gravità {SEVERITY_LABELS[sev]} ' f'· {len(items)} occorrenze

' ) by_type: dict[str, list[RecognizerResult]] = {} for r in items: by_type.setdefault(r.entity_type, []).append(r) for entity_type, results in by_type.items(): label = LABEL_IT.get(entity_type, entity_type) parts.append( f'
' f'{html.escape(label)}' f'{len(results)} occ.' f'
') parts.append('
') return _box("".join(parts))