#!/usr/bin/env bash # 构建 PRISM 出版级 PDF 全书 # 1. 预处理 14 个 markdown(剥离 emoji、改链接) # 2. pandoc 转 LaTeX body # 3. 注入模板 → XeLaTeX 编译两次(目录) # 4. 用 pypdf 合并 cover + body → PRISM_Whole.pdf # # 用法: # bash plans/PRISM/tools/build_book.sh # # 输出: # plans/PRISM/PRISM_Book.pdf 正文部分 # plans/PRISM/PRISM_Whole.pdf 封面 + 正文 完整版 set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" PRISM_DIR="$ROOT/plans/PRISM" TOOLS_DIR="$PRISM_DIR/tools" BUILD_DIR="$PRISM_DIR/.build" mkdir -p "$BUILD_DIR" # ── 版本与构建日期(注入封面/页眉/章节正文)────────────────────────── if [[ -f "$PRISM_DIR/VERSION" ]]; then VERSION="$(tr -d '[:space:]' < "$PRISM_DIR/VERSION")" else VERSION="0.0.0" fi BUILD_DATE="$(date +'%Y-%m-%d')" echo "==> Building PRISM Book v${VERSION} (${BUILD_DATE})" echo "==> 1a) 生成统计章节 16_stats.md" python3 "$TOOLS_DIR/gen_stats.py" "$PRISM_DIR" "$PRISM_DIR/16_stats.md" echo "==> 1a') 生成变更日志章节 17_changelog.md (从 CHANGELOG.md)" python3 "$TOOLS_DIR/gen_changelog_chapter.py" \ "$PRISM_DIR/CHANGELOG.md" \ "$PRISM_DIR/17_changelog.md" \ "$VERSION" "$BUILD_DATE" echo "==> 1) 预处理 markdown 文件" # 章节顺序 CHAPTERS=( "README.md" "00_overview.md" "01_capability_decomposition.md" "02_architecture.md" "03_data_schema.md" "04_pipeline_A_iphone_offline.md" "05_pipeline_B_relocalization.md" "06_pipeline_C_online_perception.md" "07_pipeline_D_consolidation.md" "08_runtime_timeline.md" "09_roadmap.md" "10_tech_stack.md" "11_world_model_bridge.md" "12_risks.md" "13_evaluation.md" "14_mvp.md" "15_glossary.md" "16_stats.md" "18_lyra_inspirations.md" "17_changelog.md" ) # Python 预处理:把每个 md 的一级标题改成 LaTeX \chapter, # 移除 emoji / 修复链接 / 删除 raw HTML python3 - <<'PYEOF' import re, sys, pathlib PRISM_DIR = pathlib.Path("plans/PRISM") OUT = PRISM_DIR / ".build" / "merged.md" # 章节顺序 order = [ "README.md", "00_overview.md", "01_capability_decomposition.md", "02_architecture.md", "03_data_schema.md", "04_pipeline_A_iphone_offline.md", "05_pipeline_B_relocalization.md", "06_pipeline_C_online_perception.md", "07_pipeline_D_consolidation.md", "08_runtime_timeline.md", "09_roadmap.md", "10_tech_stack.md", "11_world_model_bridge.md", "12_risks.md", "13_evaluation.md", "14_mvp.md", "15_glossary.md", "16_stats.md", "18_lyra_inspirations.md", "17_changelog.md", ] # 章节显示名(替换原一级标题,使目录整齐) chapter_titles = { "README.md": "项目总览 (README)", "00_overview.md": "第 0 章 总览与命名由来", "01_capability_decomposition.md": "第 1 章 两方案能力解构", "02_architecture.md": "第 2 章 四层空间记忆架构", "03_data_schema.md": "第 3 章 统一数据模型", "04_pipeline_A_iphone_offline.md": "第 4 章 管线 A:iPhone 离线建图", "05_pipeline_B_relocalization.md": "第 5 章 管线 B:ZED 重定位握手", "06_pipeline_C_online_perception.md": "第 6 章 管线 C:在线感知与差异检测", "07_pipeline_D_consolidation.md": "第 7 章 管线 D:记忆巩固", "08_runtime_timeline.md": "第 8 章 端到端运行时序", "09_roadmap.md": "第 9 章 实施路线图", "10_tech_stack.md": "第 10 章 技术栈选型", "11_world_model_bridge.md": "第 11 章 与世界模型衔接", "12_risks.md": "第 12 章 风险与对策", "13_evaluation.md": "第 13 章 评测指标", "14_mvp.md": "第 14 章 最小可复现 demo", "15_glossary.md": "第 15 章 缩略语表", "16_stats.md": "第 16 章 文档与代码统计", "18_lyra_inspirations.md": "第 18 章 Lyra 2.0 启发的设计原则", "17_changelog.md": "第 17 章 变更日志", } # 常见 emoji / 装饰符号 → 文字或删除 EMOJI_MAP = { "🌈":"", "📱":"", "📷":"", "🤖":"", "🚀":"", "💻":"", "📊":"", "📋":"", "📚":"", "🎓":"", "🎬":"", "🏗️":"", "🏨":"", "💰":"", "📐":"", "🌟":"", "⚠️":"[!]", "✅":"[OK]", "❌":"[X]", "🟢":"[L]", "🟡":"[M]", "🔴":"[H]", "🎯":"", "📈":"", "📞":"", "📍":"", "🗺️":"", "📦":"", "🔧":"", "🤝":"", "📝":"", "🌐":"", "📄":"", "📧":"", "💡":"", "💬":"", "📤":"", "📥":"", "▶":">", "◀":"<", "⭐":"★", "↑":"↑", "↓":"↓", # 第二轮补充:xelatex_pass2.log 中报告的缺失字符 "🔬":"", "🧑":"", "💼":"", "🎤":"", "📹":"", "→":"->", "↔":"<->", "①":"(1)", "②":"(2)", "③":"(3)", "④":"(4)", "✓":"[v]", "・":"·", } def preprocess(text: str, source_name: str) -> str: # 1) emoji → 替换 for k, v in EMOJI_MAP.items(): text = text.replace(k, v) # 2) 移除起首 yaml/front matter(不会有,跳过) # 3) 删除 raw HTML 注释 / details text = re.sub(r"", "", text, flags=re.S) text = re.sub(r"]*>", "", text) text = re.sub(r"]*>", "", text) # 4) 替换文件名链接 [`xx.md`](path) → \emph{xx.md} 等; # Pandoc 默认会把这些保留为超链接(指向不存在的文件), # 我们把 .md 链接收敛为只显示文本,避免大量"红色断链"。 # 直接降级为反引号代码标记,让 pandoc 自己处理转义(它会输出 \texttt{}+下划线转义) def md_link_replace(m): token = m.group(1) # 保留为 inline-code: `xxx` return f"`{token}`" text = re.sub(r"\[`([^`\]]+)`\]\([^)]+\)", md_link_replace, text) # 普通 [text](xxx.md...) 不带反引号的:仅保留文本(纯文本无需转义) text = re.sub(r"\[([^\]]+?)\]\([^)]*\.md[^)]*\)", lambda m: m.group(1), text) # 5) 把第一行 H1 替换为指定章节标题 title = chapter_titles.get(source_name, source_name) lines = text.splitlines() for i, l in enumerate(lines): if l.startswith("# "): lines[i] = f"# {title}" break else: lines.insert(0, f"# {title}") # 6) 降级:把 ## 起的所有标题保留;一级 # 后面紧跟的 "## —— xxx" 类副标题 # 保持原样,但避免 pandoc 把它认成另一个 chapter out = "\n".join(lines) # 7) 把 horizontal rule 上下的"章节版本/估计阅读时间/关键收获"等元信息 # 保留(它们是有用尾页内容) return out # 拼接 parts = [] for fn in order: p = PRISM_DIR / fn if not p.exists(): print(f"[WARN] missing {p}") continue body = p.read_text(encoding="utf-8") body = preprocess(body, fn) # 每章前换页 parts.append("\n\n\\newpage\n\n" + body + "\n") OUT.write_text("\n".join(parts), encoding="utf-8") print(f"[ok] merged → {OUT} ({OUT.stat().st_size/1024:.1f} KB)") PYEOF echo "==> 1b) 渲染 mermaid 图块 (mmdc → PNG)" FIG_DIR="$BUILD_DIR/figs" if [[ ! -x "$TOOLS_DIR/node_modules/.bin/mmdc" ]]; then echo "[ERROR] mmdc 未安装,请先执行:" echo " cd $TOOLS_DIR && npm install @mermaid-js/mermaid-cli" exit 1 fi python3 "$TOOLS_DIR/render_mermaid.py" \ "$BUILD_DIR/merged.md" \ "$BUILD_DIR/merged.md" \ "$FIG_DIR" echo "==> 2) pandoc → LaTeX body" pandoc "$BUILD_DIR/merged.md" \ --from=markdown+pipe_tables+grid_tables+raw_tex+task_lists+yaml_metadata_block \ --to=latex \ --top-level-division=chapter \ --listings=false \ --wrap=preserve \ --highlight-style=tango \ -o "$BUILD_DIR/body.tex" # 把 body 注入模板,同时把 __PRISM_VERSION__ / __PRISM_BUILD_DATE__ 占位符替换为真实值; # 并对 pandoc 生成的"自然宽度 longtable"做自适应换行后处理。 export PRISM_VERSION="$VERSION" export PRISM_BUILD_DATE="$BUILD_DATE" python3 - <<'PYEOF' import os, pathlib, re tpl = pathlib.Path("plans/PRISM/tools/book_template.tex").read_text(encoding="utf-8") body = pathlib.Path("plans/PRISM/.build/body.tex").read_text(encoding="utf-8") version = os.environ.get("PRISM_VERSION", "0.0.0") build_date = os.environ.get("PRISM_BUILD_DATE", "unknown") tpl = tpl.replace("__PRISM_VERSION__", version) tpl = tpl.replace("__PRISM_BUILD_DATE__", build_date) # pandoc 默认生成 \begin{longtable}[]{@{}lll@{}} 这种自然宽列规范, # 长内容(URL/中文长句/代码)会冲出页面。把所有形如 @{}[lcr]+@{} 的 # 自然宽列规范替换为等宽 p{w} 列,启用单元格内换行。 LT_HEAD_RE = re.compile( r"\\begin\{longtable\}(\[[^\]]*\])?\{@\{\}([lcr]+)@\{\}\}" ) def _replace_lt(m): opts = m.group(1) or "" cols = m.group(2) n = len(cols) col_spec = " ".join( r">{\raggedright\arraybackslash}p{(\linewidth - " f"{2*n}" + r"\tabcolsep)/" + f"{n}" + r"}" for _ in range(n) ) return f"\\begin{{longtable}}{opts}{{@{{}}{col_spec}@{{}}}}" n_fixed = len(LT_HEAD_RE.findall(body)) body = LT_HEAD_RE.sub(_replace_lt, body) print(f"[longtable] auto-wrapped {n_fixed} natural-width tables -> p columns") out = tpl.replace("__PRISM_BODY_PLACEHOLDER__", body) pathlib.Path("plans/PRISM/.build/book.tex").write_text(out, encoding="utf-8") print("[ok] injected -> plans/PRISM/.build/book.tex") PYEOF echo "==> 3) XeLaTeX 编译 (两次,生成目录)" cd "$BUILD_DIR" # 用 -interaction=nonstopmode 跑两遍;第二遍解决 \tableofcontents 引用 for i in 1 2; do echo " pass $i ..." xelatex -interaction=nonstopmode -halt-on-error -file-line-error \ book.tex > "xelatex_pass${i}.log" 2>&1 || { echo "[ERROR] XeLaTeX pass $i failed; tail of log:" tail -60 "xelatex_pass${i}.log" exit 1 } done cd - >/dev/null cp "$BUILD_DIR/book.pdf" "$PRISM_DIR/PRISM_Book.pdf" echo "[ok] body PDF → $PRISM_DIR/PRISM_Book.pdf ($(du -h "$PRISM_DIR/PRISM_Book.pdf" | cut -f1))" echo "==> 4) 合并 cover + body → PRISM_Whole.pdf" python3 - <')}") print(f" Subject: {src_meta.get('/Subject', '')}") PYEOF echo "" echo "============================================================" echo " ✅ PRISM 全书构建完成" echo "------------------------------------------------------------" echo " 封面: $PRISM_DIR/PRISM_Cover.pdf" echo " 正文: $PRISM_DIR/PRISM_Book.pdf" echo " 整合全本: $PRISM_DIR/PRISM_Whole.pdf" echo "============================================================"