File size: 6,766 Bytes
1e1b8a7 3f7f5a0 1e1b8a7 3f7f5a0 1e1b8a7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | """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"(?<!\*)\*(?!\s|\*)(.+?)(?<!\s|\*)\*(?!\*)"
r"|(?<![\w\u0590-\u05FF])_(?!_)(.+?)(?<!_)_(?![\w\u0590-\u05FF])"
)
def _e(text: object) -> str:
return html.escape(str(text or ""), quote=True)
def render_notice(message: str, css_class: str = "notice") -> str:
return f'<div class="{css_class}" dir="rtl" role="status">{_e(message)}</div>'
def _inline(escaped_line: str) -> str:
def bold(match: re.Match[str]) -> str:
return f"<strong>{match.group(1) if match.group(1) is not None else match.group(2)}</strong>"
def italic(match: re.Match[str]) -> str:
return f"<em>{match.group(1) if match.group(1) is not None else match.group(2)}</em>"
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"<li>{_inline(pattern.match(lines[i]).group(1))}</li>") # type: ignore[union-attr]
i += 1
blocks.append(f"<{tag}>{''.join(items)}</{tag}>")
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("<p>" + "<br>".join(para) + "</p>")
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'<a class="episode-link" href="{_e(ep.url.strip())}" target="_blank" '
f'rel="noopener noreferrer">{inner}</a>')
return f'<span class="episode-name">{inner}</span>'
def _date(ep: EpisodeView) -> str:
date = strip_fffd(ep.date).strip()
return "" if is_http_url(date) else f'<span class="src-date" dir="ltr">{_e(date)}</span>'
def _cell(label: str, content: str, css: str = "") -> str:
cls = f' class="{css}"' if css else ""
return f'<td data-label="{_e(label)}"{cls}>{content}</td>'
def _table(css: str, heading: str, headers: list[str], rows: list[str]) -> str:
head = "".join(f"<th>{_e(h)}</th>" for h in headers)
return (f'<section class="{css}"><h2>{_e(heading)}</h2><div class="table-wrap"><table>'
f'<thead><tr>{head}</tr></thead><tbody>{"".join(rows)}</tbody></table></div></section>')
def render_result(data: ViewData, question: str, view: dict, opts: DisplayOpts) -> str:
status_text = view["status_labels"].get(data.status, data.status)
parts = ['<div class="result" dir="rtl">',
f'<div class="status status-{_e(data.status)}" role="status">'
f'<span class="status-label">{_e(view["label_status"])}</span> {_e(status_text)}</div>']
answer_html = markdown_to_safe_html(data.answer_markdown or "") or f"<p>{_e(view['msg_refusal'])}</p>"
parts.append(f'<article class="answer-card"><h2>{_e(view["label_answer"])}</h2>'
f'<div class="answer-body">{answer_html}</div></article>')
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'<q class="quote-text">{highlight_terms(quote, question, opts)}</q>'
if c.verified:
check = f'<span class="badge ok" title="{_e(view["verified_title"])}">✓ {_e(view["verified_badge"])}</span>'
else:
check = f'<span class="badge unverified" title="{_e(view["unverified_title"])}">{_e(view["unverified_badge"])}</span>'
rows.append("<tr" + ("" if c.verified else ' class="row-unverified"') + ">"
+ _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'<span dir="ltr">{_e(c.timestamp)}</span>', "col-time")
+ "</tr>")
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("<tr>" + _cell(h[0], episode_link(t.episode, view), "col-episode") + _cell(h[1], _date(t.episode), "col-date")
+ _cell(h[2], f'<span class="chip chip-{_e(t.label)}">{_e(label)}</span>', "col-label")
+ _cell(h[3], _e(t.reason), "triage-reason") + "</tr>")
parts.append(_table("triage", view["label_triage"], h, rows))
if data.warnings:
items = "".join(f'<li dir="auto">{_e(w)}</li>' for w in data.warnings)
parts.append(f'<section class="warnings"><h2>{_e(view["label_warnings"])}</h2>'
f'<p class="warn-note">{_e(view["warnings_note"])}</p><ul>{items}</ul></section>')
parts.append("</div>")
return "".join(parts)
|