# -*- coding: utf-8 -*- r"""Markdown 表格渲染(**零可选依赖**)。 为什么需要它(2026-09-19 实逮,一次真实事故): `src/windcms/report.py` 用 `DataFrame.to_markdown()` 渲染"L4 过闸谱线"表 —— 而 pandas 的 `to_markdown` 依赖**可选包 `tabulate`**,本交付包没有它。此前 `model_run_l6.parquet` 是空的 (六层链那两步没实现),`len(l6)` 为 0 走的是 `'无'` 分支 ⇒ **没人踩到**;2026-09-19 我把 `model_run` 按口径重建、L6 真的有 14 行之后,重算链在 `report` 步当场炸: ImportError: `Import tabulate` failed. Use pip or conda to install the tabulate package. at src/windcms/report.py:504 l6[cols].to_markdown(index=False) 后果不是"少一张表",而是**整条重算链停在第 4 步**:后面的 ⑥重启 / ⑦本体(`ontology/objects.json` 等) 全不执行 ⇒ 页面上"决策链/本体/报告"一片空白。教训与 memory `optional-dep-in-hot-path` 同型: **热路径不许依赖可选包**,何况交付包是离线整包(不能 pip)。 这里用纯 Python 渲染同样的 Markdown 表(列宽按显示宽度补空格,中文按 2 列宽算)。 """ from __future__ import annotations def _w(s) -> int: """显示宽度: CJK 与全角标点算 2, 其余算 1(对齐用, 不追求像素级)。""" n = 0 for ch in str(s): n += 2 if ('\u4e00' <= ch <= '\u9fff' or '\u3000' <= ch <= '\u303f' or '\uff00' <= ch <= '\uffef') else 1 return n def to_md(rows: list[list], header: list[str] | None = None) -> str: """[[单元格…], …] → Markdown 表(首行可作表头)。空数据返回 ''。""" if not rows: return '' esc = lambda x: ('' if x is None else str(x)).replace('|', '\\|') # 竖线要转义, 否则表被拆列 body = [[esc(c) for c in r] for r in rows] if header: cols = [str(h) for h in header] body = [cols] + body ncol = max(len(r) for r in body) body = [r + [''] * (ncol - len(r)) for r in body] width = [max(_w(r[i]) for r in body) for i in range(ncol)] out = [] for i, r in enumerate(body): out.append('| ' + ' | '.join(str(c) + ' ' * (width[j] - _w(c)) for j, c in enumerate(r)) + ' |') if i == 0: out.append('|' + '|'.join('-' * (width[j] + 2) for j in range(ncol)) + '|') return '\n'.join(out) def df_to_md(df, cols=None, index: bool = False) -> str: """DataFrame → Markdown 表(`to_markdown(index=False)` 的零依赖替代)。""" if df is None or len(df) == 0: return '' d = df[cols] if cols else df head = ([d.index.name or ''] if index else []) + [str(c) for c in d.columns] rows = [] for idx, r in zip(d.index, d.itertuples(index=False)): rows.append(([idx] if index else []) + list(r)) return to_md(rows, header=head)