"""Shared Jinja2 environment with custom filters for the dashboard. Imported by both routers/pages.py and routers/api.py so the filters are registered exactly once.""" from __future__ import annotations from pathlib import Path from fastapi.templating import Jinja2Templates from markupsafe import Markup, escape from app.services.glossary import wrap_glossary TEMPLATE_DIR = Path(__file__).resolve().parent / "templates" def _fmt_price(v: float | None) -> str: """Format a price in a way that's readable in dense terminal tables. Avoids scientific notation for large round numbers (FTSE 25,962, not 2.596e+04) and keeps enough precision for FX rates like 0.8725 EUR/GBP.""" if v is None: return "—" av = abs(v) if av >= 1000: return f"{v:,.2f}" if av >= 10: return f"{v:.2f}" if av >= 1: return f"{v:.4f}" return f"{v:.4f}" def _fmt_signed(v: float | None, decimals: int = 2) -> str: if v is None: return "—" return f"{v:+,.{decimals}f}" def _fmt_money(v: float | None) -> str: if v is None: return "—" return f"{v:,.2f}" def _glossary_filter(value, tone: str | None = None): """Wrap glossary terms in NOVICE-mode AI content. Returns Markup so Jinja won't re-escape the inserted tags. Plain-text inputs are HTML-escaped first; already-Markup inputs (e.g. log.content_html) are treated as HTML and passed through wrap_glossary unchanged.""" if value is None: return Markup("") if isinstance(value, Markup): html = str(value) else: html = str(escape(value)) if (tone or "").upper() != "NOVICE": return Markup(html) return Markup(wrap_glossary(html, tone=tone)) templates = Jinja2Templates(directory=str(TEMPLATE_DIR)) templates.env.filters["price"] = _fmt_price templates.env.filters["signed"] = _fmt_signed templates.env.filters["money"] = _fmt_money templates.env.filters["glossary"] = _glossary_filter