207 lines
6.4 KiB
Python
207 lines
6.4 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
"""
|
|||
|
|
Docs as Code — 构建入口
|
|||
|
|
|
|||
|
|
读取 project.yaml → 拼接 chapters/*.md → 渲染为自包含 HTML 报告。
|
|||
|
|
纯 Python 实现,依赖 requirements.txt 中的 PyYAML, markdown, Jinja2。
|
|||
|
|
|
|||
|
|
用法:
|
|||
|
|
python doc_builder/build.py
|
|||
|
|
|
|||
|
|
输出:
|
|||
|
|
output/读出子系统编程控制模型.html (自包含 HTML,可离线分发)
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import os
|
|||
|
|
import sys
|
|||
|
|
import base64
|
|||
|
|
import re
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
import yaml
|
|||
|
|
import markdown
|
|||
|
|
from jinja2 import Environment, FileSystemLoader
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------- 配置 ----------
|
|||
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|||
|
|
CHAPTERS_DIR = ROOT / "chapters"
|
|||
|
|
ASSETS_DIR = ROOT / "assets"
|
|||
|
|
OUTPUT_DIR = ROOT / "output"
|
|||
|
|
OUTPUT_NAME = "读出子系统编程控制模型.html"
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------- 工具函数 ----------
|
|||
|
|
|
|||
|
|
def load_project_config() -> dict:
|
|||
|
|
"""读取 project.yaml 并校验。"""
|
|||
|
|
config_path = ROOT / "project.yaml"
|
|||
|
|
if not config_path.exists():
|
|||
|
|
sys.exit(f"错误: 找不到 {config_path}")
|
|||
|
|
with open(config_path, "r", encoding="utf-8") as f:
|
|||
|
|
config = yaml.safe_load(f)
|
|||
|
|
return config
|
|||
|
|
|
|||
|
|
|
|||
|
|
def resolve_image_path(md_content: str, assets_dir: Path, inline_images: bool = True) -> tuple[str, dict]:
|
|||
|
|
"""
|
|||
|
|
处理 Markdown 中的图片引用:
|
|||
|
|
- 如果 inline_images=True,将图片内嵌为 base64 data URI
|
|||
|
|
- 否则转换为相对路径引用
|
|||
|
|
|
|||
|
|
返回: (处理后的内容, {原始路径: data_uri 字典})
|
|||
|
|
"""
|
|||
|
|
image_map = {}
|
|||
|
|
|
|||
|
|
def replace_img(match):
|
|||
|
|
alt_text = match.group(1)
|
|||
|
|
img_path = match.group(2)
|
|||
|
|
|
|||
|
|
# 解析路径: ./assets/xxx.png 或 assets/xxx.png
|
|||
|
|
if img_path.startswith("./"):
|
|||
|
|
img_path = img_path[2:]
|
|||
|
|
if img_path.startswith("assets/"):
|
|||
|
|
img_path = img_path[7:]
|
|||
|
|
|
|||
|
|
full_path = assets_dir / img_path
|
|||
|
|
if not full_path.exists():
|
|||
|
|
print(f" 警告: 找不到图片 {full_path},保留原始引用")
|
|||
|
|
return match.group(0)
|
|||
|
|
|
|||
|
|
if inline_images:
|
|||
|
|
# 内嵌为 base64
|
|||
|
|
with open(full_path, "rb") as img_file:
|
|||
|
|
img_data = base64.b64encode(img_file.read()).decode("ascii")
|
|||
|
|
ext = full_path.suffix.lower()
|
|||
|
|
mime_map = {".png": "image/png", ".svg": "image/svg+xml",
|
|||
|
|
".jpg": "image/jpeg", ".jpeg": "image/jpeg",
|
|||
|
|
".gif": "image/gif"}
|
|||
|
|
mime = mime_map.get(ext, "image/png")
|
|||
|
|
data_uri = f"data:{mime};base64,{img_data}"
|
|||
|
|
image_map[img_path] = data_uri
|
|||
|
|
return f""
|
|||
|
|
else:
|
|||
|
|
return f""
|
|||
|
|
|
|||
|
|
return re.sub(r'!\[([^\]]*)\]\(\./assets/([^)]+)\)', replace_img, md_content), image_map
|
|||
|
|
|
|||
|
|
|
|||
|
|
def read_and_assemble(config: dict) -> str:
|
|||
|
|
"""按 project.yaml 的章节列表拼接所有 chapters/*.md 文件。"""
|
|||
|
|
chapters = config.get("chapters", [])
|
|||
|
|
if not chapters:
|
|||
|
|
sys.exit("错误: project.yaml 中未定义 chapters 列表")
|
|||
|
|
|
|||
|
|
parts = []
|
|||
|
|
for ch_file in chapters:
|
|||
|
|
ch_path = CHAPTERS_DIR / ch_file
|
|||
|
|
if not ch_path.exists():
|
|||
|
|
print(f" 警告: 章节文件不存在,跳过: {ch_file}")
|
|||
|
|
continue
|
|||
|
|
with open(ch_path, "r", encoding="utf-8") as f:
|
|||
|
|
content = f.read()
|
|||
|
|
parts.append(content)
|
|||
|
|
|
|||
|
|
return "\n\n".join(parts)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def extract_toc(md_content: str) -> list[dict]:
|
|||
|
|
"""从 Markdown 内容中提取目录结构 (H1, H2)。"""
|
|||
|
|
toc = []
|
|||
|
|
for line in md_content.split("\n"):
|
|||
|
|
m = re.match(r'^(#{1,3})\s+(.+)$', line)
|
|||
|
|
if m:
|
|||
|
|
level = len(m.group(1))
|
|||
|
|
title = m.group(2).strip()
|
|||
|
|
# 生成锚点:去除特殊字符,空格转连字符
|
|||
|
|
anchor = re.sub(r'[^\w\s一-鿿-]', '', title)
|
|||
|
|
anchor = anchor.strip().replace(' ', '-').lower()
|
|||
|
|
toc.append({"level": level, "title": title, "anchor": anchor})
|
|||
|
|
return toc
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------- HTML 生成 ----------
|
|||
|
|
|
|||
|
|
def generate_html(md_content: str, config: dict) -> str:
|
|||
|
|
"""将 Markdown 内容转换为完整的 HTML 页面。"""
|
|||
|
|
|
|||
|
|
# 处理图片内嵌
|
|||
|
|
processed_md, _ = resolve_image_path(md_content, ASSETS_DIR, inline_images=True)
|
|||
|
|
|
|||
|
|
# Markdown → HTML
|
|||
|
|
md_extensions = [
|
|||
|
|
"markdown.extensions.tables",
|
|||
|
|
"markdown.extensions.fenced_code",
|
|||
|
|
"markdown.extensions.codehilite",
|
|||
|
|
"markdown.extensions.toc",
|
|||
|
|
"markdown.extensions.nl2br",
|
|||
|
|
]
|
|||
|
|
html_body = markdown.markdown(processed_md, extensions=md_extensions)
|
|||
|
|
|
|||
|
|
# 提取目录
|
|||
|
|
toc = extract_toc(md_content)
|
|||
|
|
|
|||
|
|
# 加载 Jinja2 模板
|
|||
|
|
templates_dir = ROOT / "doc_builder" / "templates"
|
|||
|
|
env = Environment(loader=FileSystemLoader(str(templates_dir)))
|
|||
|
|
template = env.get_template("report.html")
|
|||
|
|
|
|||
|
|
# 读取主题 CSS
|
|||
|
|
themes_dir = ROOT / "doc_builder" / "themes"
|
|||
|
|
css_screen = ""
|
|||
|
|
css_print = ""
|
|||
|
|
screen_css_path = themes_dir / "report.css"
|
|||
|
|
print_css_path = themes_dir / "print.css"
|
|||
|
|
if screen_css_path.exists():
|
|||
|
|
css_screen = screen_css_path.read_text(encoding="utf-8")
|
|||
|
|
if print_css_path.exists():
|
|||
|
|
css_print = print_css_path.read_text(encoding="utf-8")
|
|||
|
|
|
|||
|
|
return template.render(
|
|||
|
|
title=config.get("title", "文档"),
|
|||
|
|
subtitle=config.get("subtitle", ""),
|
|||
|
|
author=config.get("author", ""),
|
|||
|
|
version=config.get("version", ""),
|
|||
|
|
toc=toc,
|
|||
|
|
body=html_body,
|
|||
|
|
css_screen=css_screen,
|
|||
|
|
css_print=css_print,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------- 主入口 ----------
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
print("=== ez-Q 2.5 读出子系统编程控制模型 构建 ===")
|
|||
|
|
print()
|
|||
|
|
|
|||
|
|
# 1. 加载配置
|
|||
|
|
print("[1/3] 读取 project.yaml ...")
|
|||
|
|
config = load_project_config()
|
|||
|
|
print(f" 项目: {config.get('title', '未命名')}")
|
|||
|
|
print(f" 版本: {config.get('version', 'N/A')}")
|
|||
|
|
print(f" 章节数: {len(config.get('chapters', []))}")
|
|||
|
|
|
|||
|
|
# 2. 拼接章节
|
|||
|
|
print("[2/3] 拼接章节 ...")
|
|||
|
|
assembled = read_and_assemble(config)
|
|||
|
|
print(f" 总字符数: {len(assembled)}")
|
|||
|
|
|
|||
|
|
# 3. 生成 HTML
|
|||
|
|
print("[3/3] 生成 HTML ...")
|
|||
|
|
html = generate_html(assembled, config)
|
|||
|
|
|
|||
|
|
# 4. 输出
|
|||
|
|
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
|||
|
|
output_path = OUTPUT_DIR / OUTPUT_NAME
|
|||
|
|
output_path.write_text(html, encoding="utf-8")
|
|||
|
|
print(f" 输出: {output_path}")
|
|||
|
|
print(f" 文件大小: {output_path.stat().st_size:,} 字节")
|
|||
|
|
|
|||
|
|
print()
|
|||
|
|
print("Build completed successfully!")
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|