"""Turn untrusted API JSON (via view_model) into RTL HTML. Escape first; only our tags are added. Layout follows the 7881 debug playground: status, answer, claims table (with a verified column), episode triage table, warnings. Styling follows the 2026-09-28 Space redesign. """ from __future__ import annotations import html import re from urllib.parse import urlsplit from textproc import DisplayOpts, highlight_terms, strip_fffd, trim_at_word_boundary from view_model import EpisodeView, ViewData _BULLET = re.compile(r"^\s*[-*\u2022]\s+(.*)$") _NUMBERED = re.compile(r"^\s*\d+\.\s+(.*)$") _BOLD = re.compile(r"\*\*(.+?)\*\*|__(.+?)__") _ITALIC = re.compile( r"(? str: return html.escape(str(text or ""), quote=True) def render_notice(message: str, css_class: str = "notice") -> str: return f'
{_e(message)}
' def _inline(escaped_line: str) -> str: def bold(match: re.Match[str]) -> str: return f"{match.group(1) if match.group(1) is not None else match.group(2)}" def italic(match: re.Match[str]) -> str: return f"{match.group(1) if match.group(1) is not None else match.group(2)}" return _ITALIC.sub(italic, _BOLD.sub(bold, escaped_line)) def markdown_to_safe_html(text: str) -> str: """Escape HTML, then a small Markdown subset. Never emits links or raw tags.""" lines = html.escape(strip_fffd(text).replace("\r\n", "\n").replace("\r", "\n"), quote=True).split("\n") blocks: list[str] = [] i = 0 while i < len(lines): if not lines[i].strip(): i += 1 continue for pattern, tag in ((_BULLET, "ul"), (_NUMBERED, "ol")): if pattern.match(lines[i]): items = [] while i < len(lines) and pattern.match(lines[i]): items.append(f"
  • {_inline(pattern.match(lines[i]).group(1))}
  • ") # type: ignore[union-attr] i += 1 blocks.append(f"<{tag}>{''.join(items)}") break else: para = [] while i < len(lines) and lines[i].strip() and not (_BULLET.match(lines[i]) or _NUMBERED.match(lines[i])): para.append(_inline(lines[i])) i += 1 blocks.append("

    " + "
    ".join(para) + "

    ") return "".join(blocks) def is_http_url(url: str) -> bool: candidate = (url or "").strip() if not candidate or any(ch.isspace() or ord(ch) < 32 for ch in candidate): return False try: parts = urlsplit(candidate) except ValueError: return False return parts.scheme in ("http", "https") and bool(parts.netloc) def episode_link(ep: EpisodeView, view: dict) -> str: title = strip_fffd(ep.title).strip() if not title or is_http_url(title): title = str(view["missing_title"]) inner = _e(title) if is_http_url(ep.url): return (f'{inner}') return f'{inner}' def _date(ep: EpisodeView) -> str: date = strip_fffd(ep.date).strip() return "" if is_http_url(date) else f'{_e(date)}' def _cell(label: str, content: str, css: str = "") -> str: cls = f' class="{css}"' if css else "" return f'{content}' def _table(css: str, heading: str, headers: list[str], rows: list[str]) -> str: head = "".join(f"{_e(h)}" for h in headers) return (f'

    {_e(heading)}

    ' f'{head}{"".join(rows)}
    ') def render_result(data: ViewData, question: str, view: dict, opts: DisplayOpts) -> str: status_text = view["status_labels"].get(data.status, data.status) parts = ['
    ', f'
    ' f'{_e(view["label_status"])} {_e(status_text)}
    '] answer_html = markdown_to_safe_html(data.answer_markdown or "") or f"

    {_e(view['msg_refusal'])}

    " parts.append(f'

    {_e(view["label_answer"])}

    ' f'
    {answer_html}
    ') if data.claims: h = view["claims_headers"] rows = [] for c in data.claims: quote = trim_at_word_boundary(strip_fffd(c.quote), opts.max_quote_chars, opts.ellipsis) quote_html = f'{highlight_terms(quote, question, opts)}' if c.verified: check = f'✓ {_e(view["verified_badge"])}' else: check = f'{_e(view["unverified_badge"])}' rows.append("" + _cell(h[0], _e(c.text), "claim-text") + _cell(h[1], quote_html, "claim-quote") + _cell(h[2], check, "claim-verified") + _cell(h[3], episode_link(c.episode, view), "col-episode") + _cell(h[4], _date(c.episode), "col-date") + _cell(h[5], f'{_e(c.timestamp)}', "col-time") + "") parts.append(_table("claims", view["label_claims"], h, rows)) if data.triage: h = view["triage_headers"] rows = [] for t in data.triage: label = view["triage_labels"].get(t.label, t.label) rows.append("" + _cell(h[0], episode_link(t.episode, view), "col-episode") + _cell(h[1], _date(t.episode), "col-date") + _cell(h[2], f'{_e(label)}', "col-label") + _cell(h[3], _e(t.reason), "triage-reason") + "") parts.append(_table("triage", view["label_triage"], h, rows)) if data.warnings: items = "".join(f'
  • {_e(w)}
  • ' for w in data.warnings) parts.append(f'

    {_e(view["label_warnings"])}

    ' f'

    {_e(view["warnings_note"])}

      {items}
    ') parts.append("
    ") return "".join(parts)