""" components/render_utils.py -------------------------- 純渲染工具函式,不依賴任何 Dash callback 或 app 狀態。 供 cards.py、dashTest.py(未來統一後)等元件共用。 公開 API: _inline_math(latex, style=None) → Dash element _block_math(latex, style=None) → Dash element _runs_to_children(runs) → List[Dash element] """ from __future__ import annotations from typing import Any from dash import html, dcc import dash_mantine_components as dmc try: from dash_latex import DashLatex except Exception: DashLatex = None # --------------------------------------------------------------------------- # 數學渲染 # --------------------------------------------------------------------------- def _inline_math(latex: str, style: dict | None = None) -> Any: """行內數學式。優先使用 DashLatex(KaTeX),退回 dcc.Markdown MathJax。""" merged = {"display": "inline-block", "margin": "0 2px", **(style or {})} if DashLatex: return html.Span(DashLatex(latex, displayMode=False), style=merged) src = latex if latex.strip().startswith("$") else f"${latex}$" return dcc.Markdown(src, mathjax=True, style=merged) def _block_math(latex: str, style: dict | None = None) -> Any: """區塊數學式。優先使用 DashLatex(KaTeX),退回 dcc.Markdown MathJax。""" merged = {"margin": "8px 0", **(style or {})} if DashLatex: return html.Div(DashLatex(latex, displayMode=True), style=merged) src = latex.strip() if not (src.startswith("$$") and src.endswith("$$")): src = f"$$\n{src}\n$$" return dcc.Markdown(src, mathjax=True, style=merged) # --------------------------------------------------------------------------- # Run 列表轉 Dash 元素 # --------------------------------------------------------------------------- def _runs_to_children(runs: list[dict] | None) -> list[Any]: """ 把 JSON block 中的 runs 陣列轉成 Dash 子元素清單。 Run 格式: {"kind": "math", "latex": "..."} → 行內數學 {"text": "...", "style": "bold|italic|bold-italic", "color_light": "#xxx", "color_dark": "#xxx"} → 文字 span """ out: list[Any] = [] for r in runs or []: # 數學 run if r.get("kind") == "math" and "latex" in r: out.append(_inline_math(r["latex"])) continue # 文字 run sty: dict[str, Any] = {} s = r.get("style", "") if s == "bold": sty["fontWeight"] = 700 elif s == "italic": sty["fontStyle"] = "italic" elif s == "bold-italic": sty.update({"fontWeight": 700, "fontStyle": "italic"}) # 主題雙色(light / dark) cls = None if r.get("color_light") or r.get("color_dark"): if r.get("color_light"): sty["--tc-light"] = r["color_light"] if r.get("color_dark"): sty["--tc-dark"] = r["color_dark"] sty["color"] = "var(--tc, var(--tc-light))" cls = "themed-color" out.append(html.Span(r.get("text", ""), style=sty, className=cls)) return out