chore: initial commit — import worldmodel workspace (plans/, research/)

This commit is contained in:
gaojie
2026-05-20 21:43:57 +08:00
commit bec8a9a4a3
98 changed files with 44128 additions and 0 deletions
+132
View File
@@ -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]),
)