chore: initial commit — import worldmodel workspace (plans/, research/)
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
render_mermaid.py
|
||||
=================
|
||||
扫描合并后的 markdown 中的 ```mermaid 代码块,用 mmdc(@mermaid-js/mermaid-cli)
|
||||
渲染为 PNG,并把这些块替换为可被 pandoc 直接吃下的 LaTeX includegraphics 命令。
|
||||
|
||||
用法(被 build_book.sh 调用):
|
||||
python3 render_mermaid.py <input_md> <out_md> <fig_dir>
|
||||
|
||||
特性:
|
||||
- 内容 hash 缓存,二次构建不重复渲染
|
||||
- 单个块渲染失败不会中断整体构建,会降级为 verbatim 源码 + 警告
|
||||
- 详细错误信息打到 stderr 方便调试
|
||||
|
||||
依赖:
|
||||
mmdc 必须在 plans/PRISM/tools/node_modules/.bin/mmdc
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import hashlib
|
||||
import pathlib
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
TOOLS_DIR = pathlib.Path(__file__).resolve().parent
|
||||
MMDC = TOOLS_DIR / "node_modules" / ".bin" / "mmdc"
|
||||
|
||||
|
||||
def render_one(code: str, fig_dir: pathlib.Path) -> pathlib.Path:
|
||||
"""渲染一个 mermaid 代码块,基于内容 hash 缓存。"""
|
||||
digest = hashlib.sha1(code.encode("utf-8")).hexdigest()[:12]
|
||||
out_png = fig_dir / f"mmd_{digest}.png"
|
||||
if out_png.exists():
|
||||
return out_png
|
||||
|
||||
src_mmd = fig_dir / f"mmd_{digest}.mmd"
|
||||
src_mmd.write_text(code, encoding="utf-8")
|
||||
|
||||
cmd = [
|
||||
str(MMDC),
|
||||
"-i", str(src_mmd),
|
||||
"-o", str(out_png),
|
||||
"-b", "white",
|
||||
"-s", "2",
|
||||
"--width", "1400",
|
||||
]
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if proc.returncode != 0:
|
||||
# 抽取 mermaid 的 Parse / Lexical error 行
|
||||
err_lines = [ln for ln in (proc.stdout + proc.stderr).splitlines()
|
||||
if "Error" in ln or "error" in ln]
|
||||
err_head = "\n ".join(err_lines[:5]) or "(no error line found)"
|
||||
sys.stderr.write(
|
||||
f"\n[mmdc ERROR] block {digest}:\n"
|
||||
f" {err_head}\n"
|
||||
f" --- mermaid source ---\n{code}\n"
|
||||
f" ----------------------\n"
|
||||
)
|
||||
raise RuntimeError(f"mmdc failed on block {digest}")
|
||||
return out_png
|
||||
|
||||
|
||||
MERMAID_RE = re.compile(
|
||||
r"```mermaid\s*\n(.*?)\n```",
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
|
||||
def process(input_md: pathlib.Path, out_md: pathlib.Path, fig_dir: pathlib.Path) -> int:
|
||||
if not MMDC.exists():
|
||||
sys.stderr.write(
|
||||
f"[ERROR] mmdc not found at {MMDC}\n"
|
||||
f" run: cd plans/PRISM/tools && npm install @mermaid-js/mermaid-cli\n"
|
||||
)
|
||||
sys.exit(2)
|
||||
fig_dir.mkdir(parents=True, exist_ok=True)
|
||||
text = input_md.read_text(encoding="utf-8")
|
||||
|
||||
ok = 0
|
||||
fail = 0
|
||||
failures: list[str] = []
|
||||
|
||||
def _sub(m: re.Match) -> str:
|
||||
nonlocal ok, fail
|
||||
code = m.group(1)
|
||||
try:
|
||||
png = render_one(code, fig_dir)
|
||||
except RuntimeError:
|
||||
fail += 1
|
||||
digest = hashlib.sha1(code.encode("utf-8")).hexdigest()[:12]
|
||||
failures.append(digest)
|
||||
# 降级:渲染失败 → 在 PDF 中插入"渲染失败"标记 + 源码 verbatim,
|
||||
# 这样书构建不会因为一个块挂掉而中断。
|
||||
return (
|
||||
f"\n\\begin{{quote}}\\textbf{{[mermaid 渲染失败 {digest}]}}"
|
||||
f"\\end{{quote}}\n\n"
|
||||
f"\\begin{{verbatim}}\n{code}\n\\end{{verbatim}}\n"
|
||||
)
|
||||
ok += 1
|
||||
rel = png.relative_to(input_md.parent)
|
||||
# 同时限制宽 (0.92 linewidth) 与高 (0.82 textheight),
|
||||
# keepaspectratio 自动取较紧者,保证任何极端长宽比的图都不溢出 A4 页面。
|
||||
return (
|
||||
f"\n\\begin{{center}}\n"
|
||||
f"\\includegraphics[width=0.92\\linewidth,"
|
||||
f"height=0.82\\textheight,keepaspectratio]{{{rel.as_posix()}}}\n"
|
||||
f"\\end{{center}}\n"
|
||||
)
|
||||
|
||||
new_text = MERMAID_RE.sub(_sub, text)
|
||||
out_md.write_text(new_text, encoding="utf-8")
|
||||
if fail:
|
||||
print(f"[mermaid] rendered {ok} ok, {fail} FAILED → 降级为 verbatim")
|
||||
print(f"[mermaid] failed digests: {', '.join(failures)}")
|
||||
else:
|
||||
print(f"[mermaid] rendered {ok} block(s) → {fig_dir}")
|
||||
return ok
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 4:
|
||||
sys.stderr.write(
|
||||
"usage: render_mermaid.py <input_md> <out_md> <fig_dir>\n"
|
||||
)
|
||||
sys.exit(1)
|
||||
process(
|
||||
pathlib.Path(sys.argv[1]),
|
||||
pathlib.Path(sys.argv[2]),
|
||||
pathlib.Path(sys.argv[3]),
|
||||
)
|
||||
Reference in New Issue
Block a user