diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..e08b3c9
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+*.pdf binary
+*.png binary
diff --git a/.gitignore b/.gitignore
index 63e98d1..d843e81 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
.DS_Store
tmp/
+__pycache__/
+*.pyc
diff --git a/assets/v2/codex-operating-system.png b/assets/v2/codex-operating-system.png
new file mode 100644
index 0000000..4621296
Binary files /dev/null and b/assets/v2/codex-operating-system.png differ
diff --git a/assets/v2/delivery-loop.png b/assets/v2/delivery-loop.png
new file mode 100644
index 0000000..ee747b8
Binary files /dev/null and b/assets/v2/delivery-loop.png differ
diff --git a/assets/v2/ninety-day-roadmap.png b/assets/v2/ninety-day-roadmap.png
new file mode 100644
index 0000000..886dd9e
Binary files /dev/null and b/assets/v2/ninety-day-roadmap.png differ
diff --git a/assets/v2/surface-map.png b/assets/v2/surface-map.png
new file mode 100644
index 0000000..729fd1a
Binary files /dev/null and b/assets/v2/surface-map.png differ
diff --git a/docs/Everything-CodeX.md b/docs/Everything-CodeX.md
index a0c5bfc..9c9e0a4 100644
--- a/docs/Everything-CodeX.md
+++ b/docs/Everything-CodeX.md
@@ -2,14 +2,30 @@
副标题: 从熟练使用到 Top 1% Codex Operator 的完整学习材料
-版本: 2026-05-17, Asia/Singapore
+版本: v2 draft, 2026-05-17, Asia/Singapore
适用对象: 想把 Codex 当成长期工程能力来训练的开发者、架构师、技术负责人、AI 工程效率负责人、准备对外讲授 Codex 工作法的人。
> 说明: OpenAI 官方名称是 Codex。本文沿用你给出的 "CodeX" 作为标题风格,但正文默认使用 "Codex"。
+
+
---
+## 阅读地图
+
+这版把第一版内容重新排成手册结构: 先建立心智模型,再搭建工作台,然后进入 workflow、自动化、安全、评估和授课。
+
+| 章节组 | 你要学到什么 | 推荐产出 |
+| --- | --- | --- |
+| 0-4 | Codex 的心智模型、能力地图、模型选择、与 everything-claude-code 的迁移关系 | 你自己的 Codex 能力地图 |
+| 5-10 | 工作台、AGENTS.md、config、rules、hooks、skills | 一个可复用项目模板 |
+| 11-17 | MCP、subagents、worktrees、prompt、CLI、App/IDE/Cloud、GitHub 工作流 | 一套端到端研发流程 |
+| 18-22 | 验证、memory、安全、经典模板、自动化 | 一套质量与安全闭环 |
+| 23-27 | 90 天训练、评分表、授课大纲、个人 playbook、原则 | 可讲授的课程框架 |
+
+> 阅读建议: 不要按功能记忆。每读完一章,都问自己: 这部分应该沉淀成 AGENTS.md、skill、hook、rule、MCP,还是自动化脚本?
+
## 0. 这份材料怎么学
这不是一份命令手册。它的目标是让你形成一套可复用、可验证、可教学的 Codex 工作系统。
@@ -68,6 +84,8 @@ everything-claude-code 给出的核心思路可以浓缩成一句话: 把 agent
截至 2026-05-17,Codex 的能力已经不是单点工具,而是多 surface 协作。
+
+
| Surface | 最适合做什么 | 你要掌握的重点 |
| --- | --- | --- |
| Codex App | 多项目、多线程、本地 worktree、review pane、automations、computer use、remote host | 把它当作 Codex 控制台 |
@@ -953,6 +971,8 @@ Hooks 安全:
## 21. 经典工作流模板
+
+
### 新功能
```text
@@ -1055,6 +1075,8 @@ Skill 适合复杂 playbook:
## 23. 90 天训练计划
+
+
### 第 1-7 天: 基础能力
目标: 熟练使用 Codex App、CLI、IDE extension。
@@ -1286,4 +1308,3 @@ Codex/
- OpenAI Codex Memories. https://developers.openai.com/codex/memories
- OpenAI Codex Security. https://developers.openai.com/codex/security
- OpenAI Help: Using Codex with your ChatGPT plan. https://help.openai.com/en/articles/11369540-using-codex-with-your-chatgpt-plan
-
diff --git a/docs/Everything-CodeX.pdf b/docs/Everything-CodeX.pdf
index 8c622b8..48246df 100644
Binary files a/docs/Everything-CodeX.pdf and b/docs/Everything-CodeX.pdf differ
diff --git a/scripts/create_v2_assets.py b/scripts/create_v2_assets.py
new file mode 100644
index 0000000..c5e4ee3
--- /dev/null
+++ b/scripts/create_v2_assets.py
@@ -0,0 +1,158 @@
+from __future__ import annotations
+
+from pathlib import Path
+
+from PIL import Image, ImageDraw, ImageFont
+
+
+ROOT = Path(__file__).resolve().parents[1]
+OUT = ROOT / "assets" / "v2"
+OUT.mkdir(parents=True, exist_ok=True)
+
+FONT_REG = "/System/Library/Fonts/Hiragino Sans GB.ttc"
+FONT_BOLD = "/System/Library/Fonts/STHeiti Medium.ttc"
+
+
+def font(size: int, bold: bool = False) -> ImageFont.FreeTypeFont:
+ return ImageFont.truetype(FONT_BOLD if bold else FONT_REG, size=size)
+
+
+def rounded(draw: ImageDraw.ImageDraw, box, radius, fill, outline=None, width=1):
+ draw.rounded_rectangle(box, radius=radius, fill=fill, outline=outline, width=width)
+
+
+def multiline(draw: ImageDraw.ImageDraw, xy, text, fnt, fill, spacing=8):
+ draw.multiline_text(xy, text, font=fnt, fill=fill, spacing=spacing)
+
+
+def save(img: Image.Image, name: str):
+ img.save(OUT / name, "PNG", optimize=True)
+
+
+def codex_os():
+ img = Image.new("RGB", (1600, 900), "#0f172a")
+ d = ImageDraw.Draw(img)
+ d.rectangle((0, 0, 1600, 900), fill="#0f172a")
+ d.ellipse((1050, -260, 1880, 570), fill="#172554")
+ d.ellipse((-260, 520, 540, 1210), fill="#0e7490")
+
+ d.text((90, 80), "Codex Operating System", font=font(66, True), fill="#f8fafc")
+ d.text((94, 160), "把模型能力变成可持续复利的工程系统", font=font(34), fill="#cbd5e1")
+
+ center = (560, 345, 1040, 595)
+ rounded(d, center, 42, "#f8fafc", "#dbeafe", 4)
+ d.text((665, 405), "Codex / GPT", font=font(54, True), fill="#0f172a")
+ d.text((645, 480), "Reasoning + Tools + Context", font=font(26), fill="#334155")
+
+ cards = [
+ ((90, 270, 390, 390), "AGENTS.md", "长期规则\n项目约束"),
+ ((90, 480, 390, 600), "Skills", "按需加载\n复用流程"),
+ ((1210, 270, 1510, 390), "MCP", "结构化工具\n外部系统"),
+ ((1210, 480, 1510, 600), "Hooks / Rules", "确定性护栏\n权限边界"),
+ ((420, 700, 720, 820), "Subagents", "并行探索\n专门角色"),
+ ((880, 700, 1180, 820), "Verification", "测试证据\n风险闭环"),
+ ]
+ for box, title, desc in cards:
+ rounded(d, box, 26, "#111827", "#38bdf8", 3)
+ d.text((box[0] + 28, box[1] + 24), title, font=font(30, True), fill="#e0f2fe")
+ d.text((box[0] + 28, box[1] + 66), desc, font=font(24), fill="#cbd5e1", spacing=4)
+
+ d.text((90, 830), "Top 1% 的差异不是 prompt 更长,而是上下文、流程、验证、记忆被系统化。", font=font(28), fill="#d1fae5")
+ save(img, "codex-operating-system.png")
+
+
+def surface_map():
+ img = Image.new("RGB", (1600, 950), "#f8fafc")
+ d = ImageDraw.Draw(img)
+ d.rectangle((0, 0, 1600, 950), fill="#f8fafc")
+ d.text((90, 70), "Codex Surface Map", font=font(58, True), fill="#0f172a")
+ d.text((94, 142), "不同入口服务不同任务,不要把所有任务都塞进一个聊天窗口。", font=font(30), fill="#475569")
+
+ headers = ["Surface", "最佳场景", "成熟用法"]
+ xs = [90, 410, 930]
+ widths = [280, 470, 580]
+ y = 230
+ row_h = 86
+ for i, h in enumerate(headers):
+ rounded(d, (xs[i], y, xs[i] + widths[i], y + 64), 16, "#0f172a")
+ d.text((xs[i] + 20, y + 16), h, font=font(28, True), fill="#f8fafc")
+ rows = [
+ ("App", "多项目、多线程、review pane", "用作日常控制台和任务总览"),
+ ("CLI", "本地工程、日志、JSONL、自动化", "接入 shell、CI、脚本和 schema 输出"),
+ ("IDE", "打开文件上下文、局部修改", "边看边改,适合小范围高频协作"),
+ ("Cloud", "后台 issue、PR、CI 修复", "把明确任务交给远程工程师"),
+ ("GitHub", "PR review、@codex fix", "把 Codex 嵌入团队研发流程"),
+ ("Mobile", "跟进长任务、批准动作", "离开电脑也能接管后台任务"),
+ ]
+ y += 82
+ for idx, row in enumerate(rows):
+ fill = "#ffffff" if idx % 2 == 0 else "#eef6ff"
+ for i, txt in enumerate(row):
+ d.rounded_rectangle((xs[i], y, xs[i] + widths[i], y + row_h - 10), radius=14, fill=fill, outline="#cbd5e1", width=2)
+ d.text((xs[i] + 20, y + 22), txt, font=font(25, i == 0), fill="#0f172a")
+ y += row_h
+
+ d.text((90, 860), "原则: 能用 CLI 确定性解决的,不一定上 MCP;需要长期复用的,沉淀为 skill。", font=font(27), fill="#0f766e")
+ save(img, "surface-map.png")
+
+
+def workflow_loop():
+ img = Image.new("RGB", (1600, 850), "#fff7ed")
+ d = ImageDraw.Draw(img)
+ d.text((90, 70), "Codex Delivery Loop", font=font(58, True), fill="#111827")
+ d.text((94, 142), "每个任务都按 Explore -> Plan -> Implement -> Review -> Verify -> Capture 形成闭环。", font=font(30), fill="#475569")
+ steps = [
+ ("Explore", "只读探索\n真实调用链"),
+ ("Plan", "拆阶段\n定义风险"),
+ ("Implement", "最小改动\nTDD 优先"),
+ ("Review", "owner-level\n找真实问题"),
+ ("Verify", "测试证据\n可复现结果"),
+ ("Capture", "沉淀 skill\n更新规则"),
+ ]
+ x0, y0 = 90, 330
+ w, h, gap = 210, 180, 38
+ colors = ["#0ea5e9", "#2563eb", "#7c3aed", "#db2777", "#ea580c", "#059669"]
+ for i, (title, desc) in enumerate(steps):
+ x = x0 + i * (w + gap)
+ rounded(d, (x, y0, x + w, y0 + h), 28, colors[i])
+ d.text((x + 26, y0 + 30), title, font=font(30, True), fill="#ffffff")
+ d.text((x + 26, y0 + 82), desc, font=font(25), fill="#f8fafc", spacing=7)
+ if i < len(steps) - 1:
+ d.line((x + w + 8, y0 + h // 2, x + w + gap - 8, y0 + h // 2), fill="#334155", width=5)
+ d.polygon([(x + w + gap - 8, y0 + h // 2), (x + w + gap - 26, y0 + h // 2 - 12), (x + w + gap - 26, y0 + h // 2 + 12)], fill="#334155")
+ rounded(d, (170, 650, 1430, 755), 28, "#ffffff", "#fed7aa", 3)
+ d.text((220, 684), "Done Definition: 代码可运行、测试有证据、diff 可 review、剩余风险说清楚。", font=font(32, True), fill="#9a3412")
+ save(img, "delivery-loop.png")
+
+
+def roadmap():
+ img = Image.new("RGB", (1600, 850), "#ecfeff")
+ d = ImageDraw.Draw(img)
+ d.text((90, 70), "90-Day Operator Roadmap", font=font(58, True), fill="#0f172a")
+ d.text((94, 142), "从会用 Codex,到能把 Codex 讲给别人听。", font=font(30), fill="#475569")
+ phases = [
+ ("Days 1-7", "基础能力", "App / CLI / IDE\nPrompt + 验证"),
+ ("Week 2", "配置能力", "AGENTS.md\nconfig + rules"),
+ ("Weeks 3-4", "工作流能力", "TDD / Review\nDomain skills"),
+ ("Month 2", "编排能力", "Subagents\nWorktrees + MCP"),
+ ("Month 3", "专家能力", "Evals\n课程与团队 playbook"),
+ ]
+ x, y = 110, 300
+ for i, (time, title, desc) in enumerate(phases):
+ x1 = x + i * 292
+ rounded(d, (x1, y, x1 + 248, y + 310), 30, "#ffffff", "#67e8f9", 4)
+ d.ellipse((x1 + 82, y - 48, x1 + 166, y + 36), fill="#0891b2")
+ d.text((x1 + 110, y - 32), str(i + 1), font=font(42, True), fill="#ffffff", anchor="mm")
+ d.text((x1 + 28, y + 58), time, font=font(25, True), fill="#155e75")
+ d.text((x1 + 28, y + 112), title, font=font(34, True), fill="#0f172a")
+ d.text((x1 + 28, y + 178), desc, font=font(25), fill="#334155", spacing=8)
+ d.text((110, 715), "验收标准: 你不仅能完成任务,还能解释失败、改造流程、沉淀成可复用资产。", font=font(31, True), fill="#0f766e")
+ save(img, "ninety-day-roadmap.png")
+
+
+if __name__ == "__main__":
+ codex_os()
+ surface_map()
+ workflow_loop()
+ roadmap()
+ print(f"wrote {OUT}")
diff --git a/scripts/render_handbook_pdf.py b/scripts/render_handbook_pdf.py
new file mode 100644
index 0000000..535222f
--- /dev/null
+++ b/scripts/render_handbook_pdf.py
@@ -0,0 +1,365 @@
+from __future__ import annotations
+
+import html
+import re
+import textwrap
+from pathlib import Path
+
+from reportlab.lib import colors
+from reportlab.lib.enums import TA_CENTER, TA_LEFT
+from reportlab.lib.pagesizes import A4
+from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
+from reportlab.lib.units import cm
+from reportlab.pdfbase import pdfmetrics
+from reportlab.pdfbase.cidfonts import UnicodeCIDFont
+from reportlab.platypus import (
+ Image,
+ PageBreak,
+ Paragraph,
+ Preformatted,
+ SimpleDocTemplate,
+ Spacer,
+ Table,
+ TableStyle,
+)
+from PIL import Image as PILImage
+
+
+ROOT = Path(__file__).resolve().parents[1]
+SRC = ROOT / "docs" / "Everything-CodeX.md"
+OUT = ROOT / "docs" / "Everything-CodeX.pdf"
+
+
+pdfmetrics.registerFont(UnicodeCIDFont("STSong-Light"))
+FONT = "STSong-Light"
+
+
+def styles():
+ base = getSampleStyleSheet()
+ return {
+ "title": ParagraphStyle(
+ "TitleCN",
+ parent=base["Title"],
+ fontName=FONT,
+ fontSize=31,
+ leading=38,
+ alignment=TA_CENTER,
+ textColor=colors.HexColor("#0f172a"),
+ spaceAfter=20,
+ wordWrap="CJK",
+ ),
+ "subtitle": ParagraphStyle(
+ "SubtitleCN",
+ parent=base["BodyText"],
+ fontName=FONT,
+ fontSize=11.5,
+ leading=17,
+ textColor=colors.HexColor("#334155"),
+ alignment=TA_CENTER,
+ wordWrap="CJK",
+ ),
+ "h2": ParagraphStyle(
+ "H2CN",
+ parent=base["Heading2"],
+ fontName=FONT,
+ fontSize=17,
+ leading=23,
+ textColor=colors.HexColor("#0f172a"),
+ spaceBefore=15,
+ spaceAfter=8,
+ wordWrap="CJK",
+ ),
+ "h3": ParagraphStyle(
+ "H3CN",
+ parent=base["Heading3"],
+ fontName=FONT,
+ fontSize=12.5,
+ leading=18,
+ textColor=colors.HexColor("#1e293b"),
+ spaceBefore=10,
+ spaceAfter=5,
+ wordWrap="CJK",
+ ),
+ "body": ParagraphStyle(
+ "BodyCN",
+ parent=base["BodyText"],
+ fontName=FONT,
+ fontSize=9.6,
+ leading=14.4,
+ textColor=colors.HexColor("#111827"),
+ spaceAfter=5.5,
+ alignment=TA_LEFT,
+ wordWrap="CJK",
+ ),
+ "small": ParagraphStyle(
+ "SmallCN",
+ parent=base["BodyText"],
+ fontName=FONT,
+ fontSize=8.1,
+ leading=11.5,
+ textColor=colors.HexColor("#1f2937"),
+ wordWrap="CJK",
+ ),
+ "bullet": ParagraphStyle(
+ "BulletCN",
+ parent=base["BodyText"],
+ fontName=FONT,
+ fontSize=9.3,
+ leading=13.8,
+ leftIndent=13,
+ firstLineIndent=-9,
+ spaceAfter=3.5,
+ textColor=colors.HexColor("#111827"),
+ wordWrap="CJK",
+ ),
+ "quote": ParagraphStyle(
+ "QuoteCN",
+ parent=base["BodyText"],
+ fontName=FONT,
+ fontSize=9.5,
+ leading=14.8,
+ leftIndent=12,
+ rightIndent=8,
+ borderColor=colors.HexColor("#93c5fd"),
+ borderWidth=1,
+ borderPadding=8,
+ backColor=colors.HexColor("#eff6ff"),
+ textColor=colors.HexColor("#1e3a8a"),
+ spaceBefore=4,
+ spaceAfter=8,
+ wordWrap="CJK",
+ ),
+ "code": ParagraphStyle(
+ "CodeCN",
+ fontName=FONT,
+ fontSize=7.2,
+ leading=9.4,
+ textColor=colors.HexColor("#111827"),
+ backColor=colors.HexColor("#f8fafc"),
+ borderColor=colors.HexColor("#cbd5e1"),
+ borderWidth=0.6,
+ borderPadding=6,
+ spaceBefore=4,
+ spaceAfter=8,
+ ),
+ }
+
+
+STYLES = styles()
+
+
+def inline(text: str) -> str:
+ text = html.escape(text)
+ text = re.sub(r"`([^`]+)`", r"\1", text)
+ text = re.sub(r"\*\*([^*]+)\*\*", r"\1", text)
+ text = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r"\1 (\2)", text)
+ return text
+
+
+def para(text: str, style: str = "body") -> Paragraph:
+ return Paragraph(inline(text), STYLES[style])
+
+
+def code_block(text: str) -> Preformatted:
+ out: list[str] = []
+ for line in text.rstrip("\n").splitlines():
+ if len(line) <= 92:
+ out.append(line)
+ else:
+ out.extend(textwrap.wrap(line, width=92, replace_whitespace=False, drop_whitespace=False))
+ return Preformatted("\n".join(out), STYLES["code"])
+
+
+def image_flowable(rel_path: str):
+ path = (SRC.parent / rel_path).resolve()
+ if not path.exists():
+ return para(f"[missing image: {rel_path}]", "quote")
+ max_w = A4[0] - 3.2 * cm
+ with PILImage.open(path) as im:
+ w, h = im.size
+ scale = min(max_w / w, 8.0 * cm / h)
+ return Image(str(path), width=w * scale, height=h * scale, hAlign="CENTER")
+
+
+def table_from(lines: list[str]):
+ rows = []
+ for line in lines:
+ stripped = line.strip()
+ if re.fullmatch(r"\|?[\s:|-]+\|?", stripped):
+ continue
+ cells = [c.strip() for c in stripped.strip("|").split("|")]
+ rows.append([Paragraph(inline(c), STYLES["small"]) for c in cells])
+ if not rows:
+ return []
+ col_count = max(len(r) for r in rows)
+ for row in rows:
+ while len(row) < col_count:
+ row.append(Paragraph("", STYLES["small"]))
+ available = A4[0] - 3.2 * cm
+ widths = [available / col_count] * col_count
+ table = Table(rows, colWidths=widths, repeatRows=1, hAlign="LEFT")
+ table.setStyle(
+ TableStyle(
+ [
+ ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#e2e8f0")),
+ ("GRID", (0, 0), (-1, -1), 0.35, colors.HexColor("#cbd5e1")),
+ ("VALIGN", (0, 0), (-1, -1), "TOP"),
+ ("LEFTPADDING", (0, 0), (-1, -1), 5),
+ ("RIGHTPADDING", (0, 0), (-1, -1), 5),
+ ("TOPPADDING", (0, 0), (-1, -1), 5),
+ ("BOTTOMPADDING", (0, 0), (-1, -1), 5),
+ ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#f8fafc")]),
+ ]
+ )
+ )
+ return [table, Spacer(1, 7)]
+
+
+def parse(text: str):
+ lines = text.splitlines()
+ story = []
+ i = 0
+ in_code = False
+ code_lines: list[str] = []
+ first_title = True
+
+ while i < len(lines):
+ raw = lines[i]
+ stripped = raw.strip()
+
+ if stripped.startswith("```"):
+ if in_code:
+ story.append(code_block("\n".join(code_lines)))
+ code_lines = []
+ in_code = False
+ else:
+ in_code = True
+ i += 1
+ continue
+
+ if in_code:
+ code_lines.append(raw)
+ i += 1
+ continue
+
+ if not stripped:
+ story.append(Spacer(1, 3))
+ i += 1
+ continue
+
+ if stripped == "---":
+ story.append(Spacer(1, 9))
+ i += 1
+ continue
+
+ img_match = re.match(r"!\[[^\]]*\]\(([^)]+)\)", stripped)
+ if img_match:
+ image_path = img_match.group(1)
+ story.append(Spacer(1, 5))
+ story.append(image_flowable(image_path))
+ story.append(Spacer(1, 9))
+ if "codex-operating-system" in image_path:
+ story.append(PageBreak())
+ i += 1
+ continue
+
+ if stripped.startswith("|") and "|" in stripped[1:]:
+ tbl = []
+ while i < len(lines) and lines[i].strip().startswith("|"):
+ tbl.append(lines[i])
+ i += 1
+ story.extend(table_from(tbl))
+ continue
+
+ if stripped.startswith("# "):
+ title = stripped[2:].strip()
+ if first_title:
+ story.append(Spacer(1, 2.0 * cm))
+ story.append(para(title, "title"))
+ first_title = False
+ else:
+ story.append(PageBreak())
+ story.append(para(title, "h2"))
+ i += 1
+ continue
+
+ if stripped.startswith("## "):
+ if not stripped.startswith("## 阅读地图") and not stripped.startswith("## 0."):
+ story.append(Spacer(1, 4))
+ story.append(para(stripped[3:].strip(), "h2"))
+ i += 1
+ continue
+
+ if stripped.startswith("### "):
+ story.append(para(stripped[4:].strip(), "h3"))
+ i += 1
+ continue
+
+ if stripped.startswith(">"):
+ story.append(para(stripped.lstrip("> ").strip(), "quote"))
+ i += 1
+ continue
+
+ if re.match(r"^[-*] ", stripped):
+ story.append(para("- " + stripped[2:].strip(), "bullet"))
+ i += 1
+ continue
+
+ if re.match(r"^\d+\. ", stripped):
+ story.append(para(stripped, "bullet"))
+ i += 1
+ continue
+
+ parts = [stripped]
+ i += 1
+ while i < len(lines):
+ nxt = lines[i].strip()
+ if (
+ not nxt
+ or nxt == "---"
+ or nxt.startswith("#")
+ or nxt.startswith("```")
+ or nxt.startswith("|")
+ or nxt.startswith("> ")
+ or nxt.startswith("![")
+ or re.match(r"^[-*] ", nxt)
+ or re.match(r"^\d+\. ", nxt)
+ ):
+ break
+ parts.append(nxt)
+ i += 1
+ if parts[0].startswith("副标题:") or parts[0].startswith("版本:") or parts[0].startswith("适用对象:"):
+ story.append(para(" ".join(parts), "subtitle"))
+ else:
+ story.append(para(" ".join(parts), "body"))
+ return story
+
+
+def footer(canvas, doc):
+ canvas.saveState()
+ canvas.setFont(FONT, 8)
+ canvas.setFillColor(colors.HexColor("#64748b"))
+ canvas.line(1.6 * cm, 1.35 * cm, A4[0] - 1.6 * cm, 1.35 * cm)
+ canvas.drawString(1.6 * cm, 1.0 * cm, "Everything CodeX v2 draft")
+ canvas.drawRightString(A4[0] - 1.6 * cm, 1.0 * cm, str(doc.page))
+ canvas.restoreState()
+
+
+def main():
+ story = parse(SRC.read_text(encoding="utf-8"))
+ doc = SimpleDocTemplate(
+ str(OUT),
+ pagesize=A4,
+ leftMargin=1.6 * cm,
+ rightMargin=1.6 * cm,
+ topMargin=1.55 * cm,
+ bottomMargin=1.75 * cm,
+ title="Everything CodeX v2",
+ author="tidus2005",
+ )
+ doc.build(story, onFirstPage=footer, onLaterPages=footer)
+ print(OUT)
+
+
+if __name__ == "__main__":
+ main()