# scripts/render/html.py """ChartData → 单文件 HTML 盘面(模板 render/templates/paipan.html)。 与 Markdown 输出同为纯计算渲染:十神/藏干/空亡/纳音/十二长生/月令旺衰 均为固定查表,不含任何命理判断(P1)。 """ from datetime import datetime from pathlib import Path from chart.luck import luck_label_for_year GAN = "甲乙丙丁戊己庚辛壬癸" ZHI = "子丑寅卯辰巳午未申酉戌亥" _WX_GAN = {"甲": "木", "乙": "木", "丙": "火", "丁": "火", "戊": "土", "己": "土", "庚": "金", "辛": "金", "壬": "水", "癸": "水"} _WX_ZHI = {"子": "水", "丑": "土", "寅": "木", "卯": "木", "辰": "土", "巳": "火", "午": "火", "未": "土", "申": "金", "酉": "金", "戌": "土", "亥": "水"} _YY = {"甲": 1, "丙": 1, "戊": 1, "庚": 1, "壬": 1, "乙": -1, "丁": -1, "己": -1, "辛": -1, "癸": -1} _SHENG = {"木": "火", "火": "土", "土": "金", "金": "水", "水": "木"} _KE = {"木": "土", "土": "水", "水": "火", "火": "金", "金": "木"} _HIDDEN = {"子": ["癸"], "丑": ["己", "癸", "辛"], "寅": ["甲", "丙", "戊"], "卯": ["乙"], "辰": ["戊", "乙", "癸"], "巳": ["丙", "戊", "庚"], "午": ["丁", "己"], "未": ["己", "丁", "乙"], "申": ["庚", "壬", "戊"], "酉": ["辛"], "戌": ["戊", "辛", "丁"], "亥": ["壬", "甲"]} _NAYIN = {"甲子": "海中金", "乙丑": "海中金", "丙寅": "炉中火", "丁卯": "炉中火", "戊辰": "大林木", "己巳": "大林木", "庚午": "路旁土", "辛未": "路旁土", "壬申": "剑锋金", "癸酉": "剑锋金", "甲戌": "山头火", "乙亥": "山头火", "丙子": "涧下水", "丁丑": "涧下水", "戊寅": "城头土", "己卯": "城头土", "庚辰": "白蜡金", "辛巳": "白蜡金", "壬午": "杨柳木", "癸未": "杨柳木", "甲申": "泉中水", "乙酉": "泉中水", "丙戌": "屋上土", "丁亥": "屋上土", "戊子": "霹雳火", "己丑": "霹雳火", "庚寅": "松柏木", "辛卯": "松柏木", "壬辰": "长流水", "癸巳": "长流水", "甲午": "沙中金", "乙未": "沙中金", "丙申": "山下火", "丁酉": "山下火", "戊戌": "平地木", "己亥": "平地木", "庚子": "壁上土", "辛丑": "壁上土", "壬寅": "金箔金", "癸卯": "金箔金", "甲辰": "覆灯火", "乙巳": "覆灯火", "丙午": "天河水", "丁未": "天河水", "戊申": "大驿土", "己酉": "大驿土", "庚戌": "钗钏金", "辛亥": "钗钏金", "壬子": "桑柘木", "癸丑": "桑柘木", "甲寅": "大溪水", "乙卯": "大溪水", "丙辰": "沙中土", "丁巳": "沙中土", "戊午": "天上火", "己未": "天上火", "庚申": "石榴木", "辛酉": "石榴木", "壬戌": "大海水", "癸亥": "大海水"} _TERRAIN_ORDER = ["长生", "沐浴", "冠带", "临官", "帝旺", "衰", "病", "死", "墓", "绝", "胎", "养"] _TERRAIN_START = {"甲": "亥", "乙": "午", "丙": "寅", "戊": "寅", "庚": "巳", "壬": "申", "丁": "酉", "己": "酉", "辛": "子", "癸": "卯"} _SEASON = {"寅": ("木", "火", "水", "金", "土"), "卯": ("木", "火", "水", "金", "土"), "巳": ("火", "土", "木", "水", "金"), "午": ("火", "土", "木", "水", "金"), "申": ("金", "水", "土", "火", "木"), "酉": ("金", "水", "土", "火", "木"), "亥": ("水", "木", "金", "土", "火"), "子": ("水", "木", "金", "土", "火"), "辰": ("土", "金", "火", "木", "水"), "戌": ("土", "金", "火", "木", "水"), "丑": ("土", "金", "火", "木", "水"), "未": ("土", "金", "火", "木", "水")} _SEASON_LABEL = ("旺", "相", "休", "囚", "死") def _wx(ch: str) -> str: wx = _WX_GAN.get(ch) or _WX_ZHI.get(ch) return {"木": "wx-mu", "火": "wx-huo", "土": "wx-tu", "金": "wx-jin", "水": "wx-shui"}.get(wx, "") def _ten_god(me: str, other: str) -> str: wx_m, wx_o = _WX_GAN[me], _WX_GAN[other] same = _YY[me] == _YY[other] if wx_m == wx_o: return "比肩" if same else "劫财" if _SHENG[wx_m] == wx_o: return "食神" if same else "伤官" if _KE[wx_m] == wx_o: return "偏财" if same else "正财" if _KE[wx_o] == wx_m: return "七杀" if same else "正官" return "偏印" if same else "正印" def _terrain(me: str, zhi: str) -> str: """日干对支的十二长生(阳顺阴逆)。""" start = _ZHI_IDX(_TERRAIN_START[me]) zi = _ZHI_IDX(zhi) off = (zi - start) % 12 if _YY[me] == 1 else (start - zi) % 12 return _TERRAIN_ORDER[off] def _ZHI_IDX(z: str) -> int: return ZHI.index(z) def _gz_void(gz: str) -> str: """一柱干支的旬空两支。""" g, z = GAN.index(gz[0]), ZHI.index(gz[1]) return ZHI[(z - g + 10) % 12] + ZHI[(z - g + 11) % 12] def _z(zhi: str, voids: str) -> str: cls = "voidz" if zhi in voids else "" return f'{zhi}' def _pillar_col(chart, key: str, label: str, cur: bool = False) -> dict: """四柱之一 → 各行的 HTML 单元格内容。""" p = getattr(chart, key) voids = chart.void.get("day", "") + chart.void.get("hour", "") return { "label": label, "cur": cur, "god": p.gan_ten_god or ("日主" if key == "day" else ""), "gan": p.gan, "zhi": p.zhi, "hidden": list(zip(p.hidden_stems, p.hidden_ten_gods)), "terrain": chart.terrain.get(key, ""), "void": _gz_void(p.gan + p.zhi), "nayin": p.na_yin or "", "shensha": chart.shen_sha.get(key, []), "voids": voids, } def _extra_col(chart, gz: str, label: str, cur: bool) -> dict: """流年/大运列(仅干支行星级信息,神煞留空)。""" me = chart.day_master voids = chart.void.get("day", "") + chart.void.get("hour", "") return { "label": label, "cur": cur, "god": _ten_god(me, gz[0]), "gan": gz[0], "zhi": gz[1], "hidden": [(h, _ten_god(me, h)) for h in _HIDDEN[gz[1]]], "terrain": _terrain(me, gz[1]), "void": _gz_void(gz), "nayin": _NAYIN[gz], "shensha": [], "voids": voids, } def _main_table(chart) -> str: this_year = datetime.now().year ly = next(((y, gz) for y, gz, _ in chart.flow_years if y == this_year), None) cur_luck = None for lp in chart.luck_pillars: if lp.start_year <= this_year: cur_luck = lp cols = [] if ly: cols.append(_extra_col(chart, ly[1], f"流年 {ly[0]}", cur=True)) if cur_luck: cols.append(_extra_col(chart, cur_luck.gan + cur_luck.zhi, "大运", cur=True)) for key, label in (("year", "年柱"), ("month", "月柱"), ("day", "日柱"), ("hour", "时柱")): cols.append(_pillar_col(chart, key, label, cur=(key == "day"))) def th(c): tag = '当前' if c["cur"] else "" return f'{c["label"]}{tag}' def row(label, fn, cls=""): tds = "".join( f'{fn(c)}' for c in cols) return f'{label}{tds}' rows = [ "" + '' + "".join(th(c) for c in cols) + "", row("主星", lambda c: f'{c["god"]}'), row("天干", lambda c: f'{c["gan"]}'), row("地支", lambda c: f'{_z(c["zhi"], c["voids"])}'), row("藏干", lambda c: '"), row("星运", lambda c: f'{c["terrain"]}'), row("空亡", lambda c: f'{c["void"]}'), row("纳音", lambda c: f'{c["nayin"]}'), row("神煞", lambda c: '
' + ("
".join(c["shensha"]) if c["shensha"] else "—") + "
"), ] return '' + "".join(rows) + "
" def _luck_strip(chart) -> str: this_year = datetime.now().year cells = [] for lp in chart.luck_pillars: gz = lp.gan + lp.zhi cur = lp.start_year <= this_year < lp.start_year + 10 cells.append( f'
' f'
{lp.start_year} · {lp.start_age:.0f} 岁起
' f'
{lp.gan}' f'{lp.zhi}
' f'
{_ten_god(chart.day_master, lp.gan)}' f' · {_terrain(chart.day_master, lp.zhi)}
') return '
' + "".join(cells) + "
" def _childhood_block(chart) -> str: if not chart.childhood_luck: return "" cells = "".join( f'
{age} 岁 · {year}
' f'
{gz[0]}' f'{gz[1]}
' for age, year, gz in chart.childhood_luck) return ('
小运' '童限(未交大运)各虚岁
' f'
{cells}
') def _flow_years(chart) -> str: this_year = datetime.now().year cells = [] for year, gz, label in chart.flow_years: cls = "cell" if year == this_year: cls += " cur" if label.startswith("交运年"): cls += " jiao" cells.append(f'
{year}
' f'
{gz[0]}' f'{gz[1]}
') return '
' + "".join(cells) + "
" def _flow_months(chart) -> str: cells = [] for label, gz in chart.flow_months: cells.append(f'
{label}
' f'
{gz[0]}' f'{gz[1]}
') return '
' + "".join(cells) + "
" def _season_bar(chart) -> str: seq = _SEASON.get(chart.month_ling) if not seq: return "" parts = " ".join(f"{wx}{lab}" for wx, lab in zip(seq, _SEASON_LABEL)) return f"月令 {chart.month_ling}:{parts}" def build_html(chart) -> str: """ChartData → 完整 HTML 文档。""" tpl = (Path(__file__).resolve().parent / "templates" / "paipan.html").read_text(encoding="utf-8") warns = [] if chart.is_late_zi: warns.append("⚠️ 晚子时:本造按子初换日归入次日;子正换日一派日柱、时柱不同。") if chart.jie_gap_warning: warns.append(chart.jie_gap_warning) warnings = "".join(f'
{w}
' for w in warns) qiyun = "" if chart.luck_pillars: qiyun = (f"起运:{chart.start_age_text}" f"({'顺行' if chart.luck_is_forward else '逆行'})") audit = (f"时区 {chart.tz_used}(夏令时:{'是' if chart.is_dst else '否'})· " f"经度修正 {chart.lon_correction_min:+.2f} 分 · 均时差 {chart.eot_minutes:+.2f} 分 · " f"日界 子初换日(23:00)· tyme4py {chart.library_version} · " f"schema {chart.schema_version}") mapping = { "TITLE": f"命盘:{chart.name}", "NAME": chart.name, "GENDER_LABEL": "乾造" if chart.gender == "男" else "坤造", "DAY_MASTER": f"{chart.day_master}{_WX_GAN[chart.day_master]}", "BIRTHPLACE": chart.birthplace, "BIRTH_CLOCK": chart.birth_clock, "TRUE_SOLAR": chart.true_solar_time, "MONTH_LING": chart.month_ling, "TAI_YUAN": chart.tai_yuan, "MING_GONG": chart.ming_gong, "QIYUN": qiyun, "WARNINGS": warnings, "MAIN_TABLE": _main_table(chart), "LUCK_NOTE": f"{chart.start_age_text}起运,当前一步高亮", "LUCK_STRIP": _luck_strip(chart), "CHILDHOOD_BLOCK": _childhood_block(chart), "FLOW_YEARS": _flow_years(chart), "FLOW_MONTHS": _flow_months(chart), "SEASON_BAR": _season_bar(chart), "AUDIT": audit, } out = tpl for k, v in mapping.items(): out = out.replace("{{" + k + "}}", str(v)) return out