格式修复
This commit is contained in:
parent
68acdb1aa3
commit
5b663f4014
|
|
@ -1,2 +1,7 @@
|
||||||
# Build output
|
# Build output
|
||||||
output/
|
output/
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
|
|
||||||
26
CLAUDE.md
26
CLAUDE.md
|
|
@ -12,10 +12,14 @@ Markdown 纯文本写作 + Git 版本控制 + Python 构建管道 → 自包含
|
||||||
project.yaml # 项目元数据与章节列表
|
project.yaml # 项目元数据与章节列表
|
||||||
chapters/ # Markdown 章节源文件(唯一编辑目标)
|
chapters/ # Markdown 章节源文件(唯一编辑目标)
|
||||||
assets/ # 图片资源
|
assets/ # 图片资源
|
||||||
|
data/ # 结构化数据源(CSV/YAML/JSON,通过 @import 引用)
|
||||||
doc_builder/ # Python 构建工具
|
doc_builder/ # Python 构建工具
|
||||||
build.py # 构建入口
|
build.py # 构建入口
|
||||||
templates/ # HTML 模板
|
templates/ # HTML 模板
|
||||||
themes/ # CSS 样式
|
themes/ # CSS 样式
|
||||||
|
renderers/ # 自定义渲染器(@import / 代码块渲染)
|
||||||
|
processors/ # 数据处理器(供渲染器复用)
|
||||||
|
checks/ # 检查脚本(构建时自动运行)
|
||||||
output/ # 构建产物(gitignore)
|
output/ # 构建产物(gitignore)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -31,7 +35,8 @@ output/ # 构建产物(gitignore)
|
||||||
- **禁止在子章节文件中使用 H1**,确保拼接后层级正确
|
- **禁止在子章节文件中使用 H1**,确保拼接后层级正确
|
||||||
|
|
||||||
### 图片规范
|
### 图片规范
|
||||||
- **一律使用项目根目录相对路径**: ``
|
- **从 chapters/ 引用项目根目录 assets/**: ``
|
||||||
|
- 此路径同时兼容标准 Markdown 预览和构建时的 base64 内嵌
|
||||||
- **禁止绝对路径**(尤其是 Windows 盘符路径如 `D:/code/...`)
|
- **禁止绝对路径**(尤其是 Windows 盘符路径如 `D:/code/...`)
|
||||||
- 构建时自动内嵌为 base64,生成自包含 HTML
|
- 构建时自动内嵌为 base64,生成自包含 HTML
|
||||||
|
|
||||||
|
|
@ -40,10 +45,11 @@ output/ # 构建产物(gitignore)
|
||||||
- 外部引用: `[文档名](path/to/doc.md)`
|
- 外部引用: `[文档名](path/to/doc.md)`
|
||||||
|
|
||||||
### 非标准 Markdown 扩展
|
### 非标准 Markdown 扩展
|
||||||
本项目采用 docs-as-code skill 规范的非标准扩展:
|
本项目采用 docs-as-code skill 规范的非标准扩展(构建时生效,Markdown 预览中可忽略):
|
||||||
- `@import "data/file.csv"` — 数据注入(预留)
|
- `@import "../data/file.csv"` — 将数据文件或 Markdown 注入当前章节
|
||||||
- `{w=50%}` — 图片属性控制(预留)
|
- `@import "../data/file.csv" using render_custom` — 使用 `doc_builder/renderers/render_custom.py` 渲染
|
||||||
- 自定义代码块渲染器(预留 `doc_builder/renderers/`)
|
- `{w=50%}` — 图片属性控制(预留)
|
||||||
|
- 自定义代码块渲染器:`doc_builder/renderers/render_<lang>.py`
|
||||||
|
|
||||||
## 构建流程
|
## 构建流程
|
||||||
|
|
||||||
|
|
@ -54,7 +60,7 @@ pip install -r requirements.txt
|
||||||
# 构建 HTML
|
# 构建 HTML
|
||||||
python doc_builder/build.py
|
python doc_builder/build.py
|
||||||
|
|
||||||
# 输出: output/读出子系统编程控制模型.html
|
# 输出: output/<标题>.html
|
||||||
```
|
```
|
||||||
|
|
||||||
## 文件组织表
|
## 文件组织表
|
||||||
|
|
@ -85,6 +91,10 @@ python doc_builder/build.py
|
||||||
| 正文内容 | `chapters/*.md` | `python doc_builder/build.py` |
|
| 正文内容 | `chapters/*.md` | `python doc_builder/build.py` |
|
||||||
| 章节顺序 | `project.yaml` → `chapters:` 列表 | 同上 |
|
| 章节顺序 | `project.yaml` → `chapters:` 列表 | 同上 |
|
||||||
| 图片 | `assets/` | 同上(自动内嵌) |
|
| 图片 | `assets/` | 同上(自动内嵌) |
|
||||||
|
| 结构化数据 | `data/*.csv` / `data/*.yaml` / `data/*.json` | 同上(通过 @import 注入) |
|
||||||
|
| 渲染器逻辑 | `doc_builder/renderers/*.py` | 同上(自动发现) |
|
||||||
|
| 数据处理逻辑 | `doc_builder/processors/*.py` | 同上(自动发现) |
|
||||||
|
| 检查规则 | `doc_builder/checks/*.py` | 同上(构建时自动运行) |
|
||||||
| HTML 样式 | `doc_builder/themes/*.css` | 同上 |
|
| HTML 样式 | `doc_builder/themes/*.css` | 同上 |
|
||||||
| HTML 模板 | `doc_builder/templates/report.html` | 同上 |
|
| HTML 模板 | `doc_builder/templates/report.html` | 同上 |
|
||||||
|
|
||||||
|
|
@ -92,5 +102,5 @@ python doc_builder/build.py
|
||||||
|
|
||||||
1. 本项目是 **硬件寄存器级编程手册**,包含大量位域表格和时序说明
|
1. 本项目是 **硬件寄存器级编程手册**,包含大量位域表格和时序说明
|
||||||
2. 平台差异(FPGA vs ASIC)使用代码块标注
|
2. 平台差异(FPGA vs ASIC)使用代码块标注
|
||||||
3. 数学公式使用 `$...$`(行内)和 `$$...$$`(块级)LaTeX 语法
|
3. 数学公式使用 `$...$`(行内)和 `$$...$$`(块级)LaTeX 语法,构建时自动保护公式不被 Markdown 转义破坏
|
||||||
4. 原有根目录下的 `*.md` 文件是 MPE 编辑器兼容的历史文件,编辑以 `chapters/` 为准
|
4. 编辑以 `chapters/` 下的文件为准,项目根目录无旧版 MPE 兼容文件
|
||||||
|
|
|
||||||
25
README.md
25
README.md
|
|
@ -15,10 +15,14 @@
|
||||||
project.yaml # 项目配置(标题、作者、版本、章节列表)
|
project.yaml # 项目配置(标题、作者、版本、章节列表)
|
||||||
chapters/ # Markdown 章节源文件
|
chapters/ # Markdown 章节源文件
|
||||||
assets/ # 图片资源
|
assets/ # 图片资源
|
||||||
|
data/ # 结构化数据源(CSV/YAML/JSON,通过 @import 引用)
|
||||||
doc_builder/ # Python 构建工具
|
doc_builder/ # Python 构建工具
|
||||||
build.py # 构建入口
|
build.py # 构建入口
|
||||||
templates/ # HTML 模板
|
templates/ # HTML 模板(含 A4 封面)
|
||||||
themes/ # CSS 样式
|
themes/ # CSS 样式(屏幕 + 打印)
|
||||||
|
renderers/ # 自定义渲染器(@import / 代码块)
|
||||||
|
processors/ # 数据处理器
|
||||||
|
checks/ # 检查脚本
|
||||||
output/ # 构建产物(.gitignore)
|
output/ # 构建产物(.gitignore)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -33,7 +37,7 @@ pip install -r requirements.txt
|
||||||
# 2. 构建
|
# 2. 构建
|
||||||
python doc_builder/build.py
|
python doc_builder/build.py
|
||||||
|
|
||||||
# 3. 打开 output/读出子系统编程控制模型.html 即可浏览
|
# 3. 打开 output/<标题>.html 即可浏览
|
||||||
```
|
```
|
||||||
|
|
||||||
### 编辑文档
|
### 编辑文档
|
||||||
|
|
@ -42,6 +46,15 @@ python doc_builder/build.py
|
||||||
- 添加/删除/重新排序章节:编辑 `project.yaml` 的 `chapters` 列表
|
- 添加/删除/重新排序章节:编辑 `project.yaml` 的 `chapters` 列表
|
||||||
- 修改样式:编辑 `doc_builder/themes/report.css`
|
- 修改样式:编辑 `doc_builder/themes/report.css`
|
||||||
- 修改页面布局:编辑 `doc_builder/templates/report.html`
|
- 修改页面布局:编辑 `doc_builder/templates/report.html`
|
||||||
|
- 图片放在 `assets/` 目录,章节中通过 `../assets/xxx.png` 引用
|
||||||
|
|
||||||
|
### 非标准 Markdown 扩展
|
||||||
|
|
||||||
|
本项目支持以下扩展语法(仅在构建时生效):
|
||||||
|
|
||||||
|
- **`@import "path"`** — 将数据文件或 Markdown 注入当前章节
|
||||||
|
- **`@import "path" using render_xxx`** — 使用自定义渲染器
|
||||||
|
- **`{w=50%}`** — 图片属性控制
|
||||||
|
|
||||||
### 编辑器推荐
|
### 编辑器推荐
|
||||||
|
|
||||||
|
|
@ -49,9 +62,3 @@ python doc_builder/build.py
|
||||||
- Typora
|
- Typora
|
||||||
- Obsidian
|
- Obsidian
|
||||||
- 任何支持 Markdown 的编辑器
|
- 任何支持 Markdown 的编辑器
|
||||||
|
|
||||||
## 向后兼容
|
|
||||||
|
|
||||||
原有的 Markdown Preview Enhanced (MPE) 导出方式仍然可用:
|
|
||||||
在 VS Code 中安装 MPE 插件后,打开 `读出子系统编程控制模型.md` 可实时预览和导出 HTML。
|
|
||||||
不过建议优先使用 `doc_builder/build.py` 进行构建。
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 686 KiB |
|
|
@ -56,7 +56,7 @@ ez-Q 2.5 ASIC平台读出子系统由读出基带板、读出混频板和读出
|
||||||
- 读出混频板自身产生一个本振信号,用于实现基带信号与射频信号之间的转换;
|
- 读出混频板自身产生一个本振信号,用于实现基带信号与射频信号之间的转换;
|
||||||
- 而读出泵浦板负责产生一个指定功率频率的单音信号,并在外部使能信号控制下输出;
|
- 而读出泵浦板负责产生一个指定功率频率的单音信号,并在外部使能信号控制下输出;
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
当软件需要对不同通道编程时,其通过ip地址指定机箱、通过槽位号指定板卡、
|
当软件需要对不同通道编程时,其通过ip地址指定机箱、通过槽位号指定板卡、
|
||||||
通过扩展地址指定同一个板卡内的多个通道、通过地址指定一个通道内的不同配置项。
|
通过扩展地址指定同一个板卡内的多个通道、通过地址指定一个通道内的不同配置项。
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
读出系统的的读出参数存储格式如下图所示。
|
读出系统的的读出参数存储格式如下图所示。
|
||||||
FPGA平台下的系数直读模式只使用`Ctrl`部分中的数据,并忽略`dds_pfw`控制字.
|
FPGA平台下的系数直读模式只使用`Ctrl`部分中的数据,并忽略`dds_pfw`控制字.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### 4.4.2. 匹配滤波器系数
|
### 4.4.2. 匹配滤波器系数
|
||||||
|
|
||||||
|
|
@ -29,4 +29,4 @@ ez-Q 2.5 FPGA平台使用系数直读模式,其需要额外的存储空间来
|
||||||
- 匹配滤波器的I和Q数据分开存储,每个比特的I、Q数据容量分别为16 KB。
|
- 匹配滤波器的I和Q数据分开存储,每个比特的I、Q数据容量分别为16 KB。
|
||||||
- I路数据偏移地址为x*32KB,Q路数据偏移地址为x*32KB+16 KB,其中x为Qubit序号,范围为0~15。
|
- I路数据偏移地址为x*32KB,Q路数据偏移地址为x*32KB+16 KB,其中x为Qubit序号,范围为0~15。
|
||||||
|
|
||||||

|

|
||||||
|
|
|
||||||
|
|
@ -6,4 +6,4 @@
|
||||||
索引表格式定义如下图所示: wave_id是mcu产生的码字,其可以作为地址索引波形控制参数。
|
索引表格式定义如下图所示: wave_id是mcu产生的码字,其可以作为地址索引波形控制参数。
|
||||||
波形控制参数包括波形地址`addr`和波形长度`len`参数,颗粒度是时钟周期。
|
波形控制参数包括波形地址`addr`和波形长度`len`参数,颗粒度是时钟周期。
|
||||||
|
|
||||||

|

|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ MCU的指令、MCU的数据、控制寄存器、波形索性表、波形仓库
|
||||||
|
|
||||||
当前模拟电路仅需配置Pump参数,下图是EXC-Pump通道数字部分的编程控制模型。
|
当前模拟电路仅需配置Pump参数,下图是EXC-Pump通道数字部分的编程控制模型。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
1. EXC-Pump通道的波形输出由AWG模块MCU发出的码字触发。
|
1. EXC-Pump通道的波形输出由AWG模块MCU发出的码字触发。
|
||||||
对于输出波形而言,MCU发出的码字定义波形的索引ID,
|
对于输出波形而言,MCU发出的码字定义波形的索引ID,
|
||||||
|
|
@ -29,7 +29,7 @@ MCU的指令、MCU的数据、控制寄存器、波形索性表、波形仓库
|
||||||
最后数字信号经过DAC转换成基带信号,基带信号再和外部本振信号模拟混频后输出读出激励波形。
|
最后数字信号经过DAC转换成基带信号,基带信号再和外部本振信号模拟混频后输出读出激励波形。
|
||||||
读出芯片不同模式输出的频响曲线如下图所示:
|
读出芯片不同模式输出的频响曲线如下图所示:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
为了兼容混频输出和射频直出两种工作模式以及在FPGA和ASIC平台上实现,半带滤波器和MIX模块都支持旁路功能,因此最终波形输出支持NRZ、MIX、HBNRZ和HBMIX四种模式。
|
为了兼容混频输出和射频直出两种工作模式以及在FPGA和ASIC平台上实现,半带滤波器和MIX模块都支持旁路功能,因此最终波形输出支持NRZ、MIX、HBNRZ和HBMIX四种模式。
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,204 +2,573 @@
|
||||||
"""
|
"""
|
||||||
Docs as Code — 构建入口
|
Docs as Code — 构建入口
|
||||||
|
|
||||||
读取 project.yaml → 拼接 chapters/*.md → 渲染为自包含 HTML 报告。
|
读取 project.yaml → 逐章节处理 @import → Markdown → HTML → 图片内嵌
|
||||||
纯 Python 实现,依赖 requirements.txt 中的 PyYAML, markdown, Jinja2。
|
→ 组装 → 注入锚点 → 提取目录 → 模板渲染 → 自包含 HTML 报告。
|
||||||
|
|
||||||
用法:
|
用法:
|
||||||
python doc_builder/build.py
|
python doc_builder/build.py
|
||||||
|
|
||||||
输出:
|
输出:
|
||||||
output/读出子系统编程控制模型.html (自包含 HTML,可离线分发)
|
output/<标题>.html (自包含 HTML,可离线分发)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import base64
|
import base64
|
||||||
|
import csv
|
||||||
|
import importlib.util
|
||||||
|
import io
|
||||||
|
import json
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
|
from datetime import date
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import yaml
|
|
||||||
import markdown
|
import markdown
|
||||||
from jinja2 import Environment, FileSystemLoader
|
import yaml
|
||||||
|
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||||
|
|
||||||
|
# ---------- 路径配置 ----------
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
CHAPTERS_DIR = PROJECT_ROOT / "chapters"
|
||||||
|
ASSETS_DIR = PROJECT_ROOT / "assets"
|
||||||
|
OUTPUT_DIR = PROJECT_ROOT / "output"
|
||||||
|
BUILDER_DIR = PROJECT_ROOT / "doc_builder"
|
||||||
|
TEMPLATES_DIR = BUILDER_DIR / "templates"
|
||||||
|
THEMES_DIR = BUILDER_DIR / "themes"
|
||||||
|
RENDERERS_DIR = BUILDER_DIR / "renderers"
|
||||||
|
CHECKS_DIR = BUILDER_DIR / "checks"
|
||||||
|
|
||||||
# ---------- 配置 ----------
|
# ---------- 配置加载 ----------
|
||||||
ROOT = Path(__file__).resolve().parent.parent
|
|
||||||
CHAPTERS_DIR = ROOT / "chapters"
|
|
||||||
ASSETS_DIR = ROOT / "assets"
|
|
||||||
OUTPUT_DIR = ROOT / "output"
|
|
||||||
OUTPUT_NAME = "读出子系统编程控制模型.html"
|
|
||||||
|
|
||||||
|
def load_config():
|
||||||
|
path = PROJECT_ROOT / "project.yaml"
|
||||||
|
if not path.exists():
|
||||||
|
raise FileNotFoundError("找不到 project.yaml")
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
config = yaml.safe_load(f) or {}
|
||||||
|
|
||||||
# ---------- 工具函数 ----------
|
config.setdefault("title", "未命名文档")
|
||||||
|
config.setdefault("subtitle", "")
|
||||||
def load_project_config() -> dict:
|
config.setdefault("author", "")
|
||||||
"""读取 project.yaml 并校验。"""
|
config.setdefault("version", "")
|
||||||
config_path = ROOT / "project.yaml"
|
config.setdefault("doc_type", "技术文档")
|
||||||
if not config_path.exists():
|
config.setdefault("logo", "")
|
||||||
sys.exit(f"错误: 找不到 {config_path}")
|
config.setdefault("lang", "zh-CN")
|
||||||
with open(config_path, "r", encoding="utf-8") as f:
|
config.setdefault("date", date.today().isoformat())
|
||||||
config = yaml.safe_load(f)
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
def resolve_image_path(md_content: str, assets_dir: Path, inline_images: bool = True) -> tuple[str, dict]:
|
# ---------- 插件发现 ----------
|
||||||
|
|
||||||
|
def load_plugin_modules(directory):
|
||||||
|
"""扫描目录下的 *.py 文件并导入为模块字典。"""
|
||||||
|
modules = {}
|
||||||
|
if not directory.exists():
|
||||||
|
return modules
|
||||||
|
for py_file in sorted(directory.glob("*.py")):
|
||||||
|
if py_file.name.startswith("_"):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
spec = importlib.util.spec_from_file_location(py_file.stem, py_file)
|
||||||
|
if spec is None or spec.loader is None:
|
||||||
|
continue
|
||||||
|
mod = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(mod)
|
||||||
|
modules[py_file.stem] = mod
|
||||||
|
print(f" 已加载: {py_file.stem}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" 警告: 加载 {py_file.name} 失败: {e}")
|
||||||
|
return modules
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- @import 处理 ----------
|
||||||
|
|
||||||
|
IMPORT_RE = re.compile(r'^@import\s+"([^"]+)"(?:\s+using\s+(\S+))?\s*$', re.MULTILINE)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_import_path(raw, base_dir):
|
||||||
|
path = Path(raw)
|
||||||
|
if path.is_absolute():
|
||||||
|
return path
|
||||||
|
return (base_dir / path).resolve()
|
||||||
|
|
||||||
|
|
||||||
|
def default_data_renderer(filepath):
|
||||||
|
"""默认数据文件渲染:CSV → HTML 表格,YAML/JSON → 代码块。"""
|
||||||
|
ext = filepath.suffix.lower()
|
||||||
|
if ext == ".csv":
|
||||||
|
with open(filepath, newline="", encoding="utf-8") as f:
|
||||||
|
reader = csv.reader(f)
|
||||||
|
rows = list(reader)
|
||||||
|
if not rows:
|
||||||
|
return ""
|
||||||
|
lines = ["<table>"]
|
||||||
|
lines.append("<tr>" + "".join(f"<th>{c}</th>" for c in rows[0]) + "</tr>")
|
||||||
|
for row in rows[1:]:
|
||||||
|
lines.append("<tr>" + "".join(f"<td>{c}</td>" for c in row) + "</tr>")
|
||||||
|
lines.append("</table>")
|
||||||
|
return "".join(lines)
|
||||||
|
elif ext in (".yaml", ".yml"):
|
||||||
|
with open(filepath, "r", encoding="utf-8") as f:
|
||||||
|
data = yaml.safe_load(f) or {}
|
||||||
|
return f"<pre><code>{yaml.dump(data, allow_unicode=True)}</code></pre>"
|
||||||
|
elif ext == ".json":
|
||||||
|
with open(filepath, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
return f"<pre><code>{json.dumps(data, ensure_ascii=False, indent=2)}</code></pre>"
|
||||||
|
else:
|
||||||
|
content = filepath.read_text(encoding="utf-8")
|
||||||
|
return f"<pre><code>{content}</code></pre>"
|
||||||
|
|
||||||
|
|
||||||
|
def render_with_module(renderers, renderer_name, filepath):
|
||||||
|
"""调用指定渲染器。"""
|
||||||
|
if renderer_name not in renderers:
|
||||||
|
raise ValueError(f"找不到指定渲染器:{renderer_name}")
|
||||||
|
func = getattr(renderers[renderer_name], "render", None)
|
||||||
|
if not callable(func):
|
||||||
|
raise ValueError(f"{renderer_name} 没有 render(content: str) -> str 函数")
|
||||||
|
return func(str(filepath))
|
||||||
|
|
||||||
|
|
||||||
|
def rewrite_image_paths(text, source_dir):
|
||||||
|
"""把被导入 Markdown 中的相对图片路径改为绝对路径,供后续 base64 嵌入。"""
|
||||||
|
def repl(match):
|
||||||
|
alt = match.group(1)
|
||||||
|
src = match.group(2)
|
||||||
|
if src.startswith(("http://", "https://", "data:")) or Path(src).is_absolute():
|
||||||
|
return match.group(0)
|
||||||
|
abs_path = (source_dir / src).resolve()
|
||||||
|
return f''
|
||||||
|
return re.sub(r'!\[([^\]]*)\]\(([^)]+)\)', repl, text)
|
||||||
|
|
||||||
|
|
||||||
|
def process_imports(text, base_dir, renderers, _imported=None):
|
||||||
|
"""扫描并替换 @import 指令。支持 .md 注入和数据文件导入。"""
|
||||||
|
if _imported is None:
|
||||||
|
_imported = set()
|
||||||
|
|
||||||
|
def repl(match):
|
||||||
|
raw = match.group(1)
|
||||||
|
renderer_name = match.group(2)
|
||||||
|
target = resolve_import_path(raw, base_dir)
|
||||||
|
|
||||||
|
# 外部 Markdown 导入
|
||||||
|
if raw.endswith(".md"):
|
||||||
|
if target in _imported:
|
||||||
|
raise RuntimeError(f"检测到循环 @import:{target}")
|
||||||
|
_imported.add(target)
|
||||||
|
if not target.exists():
|
||||||
|
raise FileNotFoundError(f"找不到要导入的 Markdown 文件:{target}")
|
||||||
|
md = target.read_text(encoding="utf-8")
|
||||||
|
md = rewrite_image_paths(md, target.parent)
|
||||||
|
md = process_imports(md, target.parent, renderers, _imported)
|
||||||
|
return md
|
||||||
|
|
||||||
|
# 数据导入
|
||||||
|
if not target.exists():
|
||||||
|
raise FileNotFoundError(f"找不到要导入的数据文件:{target}")
|
||||||
|
if renderer_name:
|
||||||
|
renderer_name = renderer_name.removesuffix(".py")
|
||||||
|
return render_with_module(renderers, renderer_name, target)
|
||||||
|
return default_data_renderer(target)
|
||||||
|
|
||||||
|
return IMPORT_RE.sub(repl, text)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- 图片属性 {w=50%} ----------
|
||||||
|
|
||||||
|
IMAGE_ATTR_RE = re.compile(r'!\[([^\]]*)\]\(([^)]+)\)\{([^}]*)\}')
|
||||||
|
|
||||||
|
|
||||||
|
def parse_attrs(attr_str):
|
||||||
|
attrs = {}
|
||||||
|
for part in attr_str.split(","):
|
||||||
|
part = part.strip()
|
||||||
|
if "=" in part:
|
||||||
|
k, v = part.split("=", 1)
|
||||||
|
attrs[k.strip()] = v.strip()
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
def process_image_attrs(text):
|
||||||
|
"""处理 {w=50%} 图片属性语法,转换为 HTML img 标签。"""
|
||||||
|
def repl(match):
|
||||||
|
alt = match.group(1)
|
||||||
|
src = match.group(2)
|
||||||
|
attr_str = match.group(3)
|
||||||
|
attrs = parse_attrs(attr_str)
|
||||||
|
style = ""
|
||||||
|
if "w" in attrs:
|
||||||
|
style = f'width:{attrs["w"]};'
|
||||||
|
cls = attrs.get("class", "")
|
||||||
|
cls_attr = f' class="{cls}"' if cls else ""
|
||||||
|
style_attr = f' style="{style}"' if style else ""
|
||||||
|
return f'<img src="{src}" alt="{alt}"{cls_attr}{style_attr} />'
|
||||||
|
return IMAGE_ATTR_RE.sub(repl, text)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- KaTeX 预处理 ----------
|
||||||
|
|
||||||
|
def preprocess_katex(text):
|
||||||
"""
|
"""
|
||||||
处理 Markdown 中的图片引用:
|
将 Markdown 中的 LaTeX 公式包装为原始 HTML,
|
||||||
- 如果 inline_images=True,将图片内嵌为 base64 data URI
|
防止 Markdown 解析器错误解释公式中的 _ * 等字符。
|
||||||
- 否则转换为相对路径引用
|
|
||||||
|
|
||||||
返回: (处理后的内容, {原始路径: data_uri 字典})
|
|
||||||
"""
|
"""
|
||||||
image_map = {}
|
# 块级公式 $$...$$
|
||||||
|
def protect_display(match):
|
||||||
|
latex = match.group(1)
|
||||||
|
return f'<div class="math-display">$${latex}$$</div>'
|
||||||
|
text = re.sub(r'\$\$\s*(.+?)\s*\$\$', protect_display, text, flags=re.DOTALL)
|
||||||
|
|
||||||
def replace_img(match):
|
# 行内公式 $...$
|
||||||
alt_text = match.group(1)
|
def protect_inline(match):
|
||||||
img_path = match.group(2)
|
latex = match.group(1)
|
||||||
|
return f'<span class="math-inline">${latex}$</span>'
|
||||||
|
text = re.sub(r'(?<!\d)\$([^$\s].*?[^$\s])\$(?!\d)', protect_inline, text)
|
||||||
|
|
||||||
# 解析路径: ./assets/xxx.png 或 assets/xxx.png
|
return text
|
||||||
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},保留原始引用")
|
|
||||||
|
FENCED_CODEBLOCK_OPEN_RE = re.compile(r'^```(\w+)(?:\s+[^\n]*)?$')
|
||||||
|
|
||||||
|
|
||||||
|
def render_custom_codeblocks_md(text, renderers):
|
||||||
|
"""把有对应渲染器的 fenced code block 替换为 HTML。"""
|
||||||
|
lines = text.splitlines(keepends=True)
|
||||||
|
out_lines = []
|
||||||
|
i = 0
|
||||||
|
while i < len(lines):
|
||||||
|
line = lines[i]
|
||||||
|
m = FENCED_CODEBLOCK_OPEN_RE.match(line)
|
||||||
|
if not m:
|
||||||
|
out_lines.append(line)
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
lang = m.group(1)
|
||||||
|
renderer_name = f"render_{lang.replace('-', '_')}"
|
||||||
|
if renderer_name not in renderers:
|
||||||
|
out_lines.append(line)
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
func = getattr(renderers[renderer_name], "render", None)
|
||||||
|
if not callable(func):
|
||||||
|
out_lines.append(line)
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 收集到闭合 fence
|
||||||
|
start = i + 1
|
||||||
|
j = start
|
||||||
|
while j < len(lines) and lines[j].strip() != '```':
|
||||||
|
j += 1
|
||||||
|
if j >= len(lines):
|
||||||
|
out_lines.append(line)
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
content = "".join(lines[start:j]).rstrip("\n")
|
||||||
|
rendered = func(content)
|
||||||
|
if not rendered.endswith("\n"):
|
||||||
|
rendered += "\n"
|
||||||
|
out_lines.append(rendered)
|
||||||
|
# 跳过 fence 后的换行
|
||||||
|
if j + 1 < len(lines) and lines[j + 1] == "\n":
|
||||||
|
i = j + 2
|
||||||
|
else:
|
||||||
|
i = j + 1
|
||||||
|
|
||||||
|
return "".join(out_lines)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Markdown → HTML ----------
|
||||||
|
|
||||||
|
def markdown_to_html(text):
|
||||||
|
md = markdown.Markdown(extensions=[
|
||||||
|
"extra",
|
||||||
|
"tables",
|
||||||
|
"fenced_code",
|
||||||
|
"toc",
|
||||||
|
])
|
||||||
|
return md.convert(text)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- 图片 base64 内嵌(HTML 级别) ----------
|
||||||
|
|
||||||
|
IMG_SRC_RE = re.compile(r'<img([^>]*?)src=["\']([^"\']+)["\']([^>]*)>', re.IGNORECASE)
|
||||||
|
|
||||||
|
MIME_TABLE = {
|
||||||
|
".png": "image/png",
|
||||||
|
".jpg": "image/jpeg",
|
||||||
|
".jpeg": "image/jpeg",
|
||||||
|
".gif": "image/gif",
|
||||||
|
".svg": "image/svg+xml",
|
||||||
|
".webp": "image/webp",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def guess_mime(ext):
|
||||||
|
return MIME_TABLE.get(ext.lower(), "application/octet-stream")
|
||||||
|
|
||||||
|
|
||||||
|
def embed_images(html, base_dirs):
|
||||||
|
"""扫描 HTML 中的 img 标签,将本地图片替换为 base64 data URI。"""
|
||||||
|
def resolve_src(src):
|
||||||
|
if Path(src).is_absolute():
|
||||||
|
path = Path(src)
|
||||||
|
if path.exists():
|
||||||
|
return path
|
||||||
|
return None
|
||||||
|
for base in base_dirs:
|
||||||
|
path = base / src
|
||||||
|
if path.exists():
|
||||||
|
return path
|
||||||
|
return None
|
||||||
|
|
||||||
|
def repl(match):
|
||||||
|
prefix = match.group(1)
|
||||||
|
src = match.group(2)
|
||||||
|
suffix = match.group(3)
|
||||||
|
|
||||||
|
if src.startswith(("http://", "https://", "data:")):
|
||||||
return match.group(0)
|
return match.group(0)
|
||||||
|
|
||||||
if inline_images:
|
path = resolve_src(src)
|
||||||
# 内嵌为 base64
|
if path is None:
|
||||||
with open(full_path, "rb") as img_file:
|
print(f" [警告] 找不到图片,保留原路径:{src}")
|
||||||
img_data = base64.b64encode(img_file.read()).decode("ascii")
|
return match.group(0)
|
||||||
ext = full_path.suffix.lower()
|
try:
|
||||||
mime_map = {".png": "image/png", ".svg": "image/svg+xml",
|
mime = guess_mime(path.suffix)
|
||||||
".jpg": "image/jpeg", ".jpeg": "image/jpeg",
|
data = path.read_bytes()
|
||||||
".gif": "image/gif"}
|
b64 = base64.b64encode(data).decode("ascii")
|
||||||
mime = mime_map.get(ext, "image/png")
|
return f'<img{prefix}src="data:{mime};base64,{b64}"{suffix}>'
|
||||||
data_uri = f"data:{mime};base64,{img_data}"
|
except Exception as e:
|
||||||
image_map[img_path] = data_uri
|
print(f" [警告] 图片 base64 编码失败({src}):{e}")
|
||||||
return f""
|
return match.group(0)
|
||||||
else:
|
|
||||||
return f""
|
|
||||||
|
|
||||||
return re.sub(r'!\[([^\]]*)\]\(\./assets/([^)]+)\)', replace_img, md_content), image_map
|
return IMG_SRC_RE.sub(repl, html)
|
||||||
|
|
||||||
|
|
||||||
def read_and_assemble(config: dict) -> str:
|
# ---------- 目录与锚点 ----------
|
||||||
"""按 project.yaml 的章节列表拼接所有 chapters/*.md 文件。"""
|
|
||||||
chapters = config.get("chapters", [])
|
|
||||||
if not chapters:
|
|
||||||
sys.exit("错误: project.yaml 中未定义 chapters 列表")
|
|
||||||
|
|
||||||
parts = []
|
def slugify(text):
|
||||||
for ch_file in chapters:
|
"""生成 HTML 锚点 ID。"""
|
||||||
ch_path = CHAPTERS_DIR / ch_file
|
anchor = re.sub(r'[^\w\s一-鿿-]', '', text)
|
||||||
if not ch_path.exists():
|
return anchor.strip().replace(" ", "-")[:50]
|
||||||
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]:
|
def generate_toc(html):
|
||||||
"""从 Markdown 内容中提取目录结构 (H1, H2)。"""
|
"""从 HTML 中提取 H1-H4 标题,生成目录。"""
|
||||||
toc = []
|
toc = []
|
||||||
for line in md_content.split("\n"):
|
for m in re.finditer(r'<h([1-4])[^>]*>(.*?)</h\1>', html, re.DOTALL):
|
||||||
m = re.match(r'^(#{1,3})\s+(.+)$', line)
|
level = int(m.group(1))
|
||||||
if m:
|
text = re.sub(r'<.*?>', '', m.group(2)).strip()
|
||||||
level = len(m.group(1))
|
toc.append({"level": level, "text": text, "anchor": slugify(text)})
|
||||||
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
|
return toc
|
||||||
|
|
||||||
|
|
||||||
# ---------- HTML 生成 ----------
|
def inject_anchors(html):
|
||||||
|
"""为所有 H1-H4 标签注入 id 属性,使侧边栏目录可跳转。"""
|
||||||
def generate_html(md_content: str, config: dict) -> str:
|
def repl(match):
|
||||||
"""将 Markdown 内容转换为完整的 HTML 页面。"""
|
level = match.group(1)
|
||||||
|
attrs = match.group(2)
|
||||||
# 处理图片内嵌
|
inner = match.group(3)
|
||||||
processed_md, _ = resolve_image_path(md_content, ASSETS_DIR, inline_images=True)
|
anchor = slugify(re.sub(r'<.*?>', '', inner).strip())
|
||||||
|
return f'<h{level} id="{anchor}"{attrs}>{inner}</h{level}>'
|
||||||
# Markdown → HTML
|
return re.sub(
|
||||||
md_extensions = [
|
r'<h([1-4])([^>]*)>(.*?)</h\1>',
|
||||||
"markdown.extensions.tables",
|
repl,
|
||||||
"markdown.extensions.fenced_code",
|
html,
|
||||||
"markdown.extensions.codehilite",
|
flags=re.DOTALL,
|
||||||
"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,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Logo 处理 ----------
|
||||||
|
|
||||||
|
def find_logo(logo_path_str):
|
||||||
|
"""定位 logo 文件;需在 project.yaml 明确指定 logo 字段。"""
|
||||||
|
if not logo_path_str:
|
||||||
|
return None
|
||||||
|
path = Path(logo_path_str)
|
||||||
|
if path.is_absolute():
|
||||||
|
return path
|
||||||
|
return PROJECT_ROOT / path
|
||||||
|
|
||||||
|
|
||||||
|
def embed_logo(logo_path, max_width=400):
|
||||||
|
"""压缩 logo 并返回 base64 data URI。"""
|
||||||
|
if logo_path is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PIL import Image
|
||||||
|
except ImportError:
|
||||||
|
print(" 提示: 未安装 Pillow,跳过 logo 处理")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
img = Image.open(logo_path)
|
||||||
|
w, h = img.size
|
||||||
|
if w > max_width:
|
||||||
|
ratio = max_width / w
|
||||||
|
img = img.resize((max_width, int(h * ratio)), Image.Resampling.LANCZOS)
|
||||||
|
|
||||||
|
ext = logo_path.suffix.lower()
|
||||||
|
if ext == ".svg":
|
||||||
|
data = logo_path.read_bytes()
|
||||||
|
b64 = base64.b64encode(data).decode("ascii")
|
||||||
|
return f"data:image/svg+xml;base64,{b64}"
|
||||||
|
|
||||||
|
buf = io.BytesIO()
|
||||||
|
if img.mode in ("RGBA", "P"):
|
||||||
|
img.save(buf, format="PNG", optimize=True)
|
||||||
|
mime = "image/png"
|
||||||
|
else:
|
||||||
|
img = img.convert("RGB")
|
||||||
|
img.save(buf, format="JPEG", optimize=True, quality=90)
|
||||||
|
mime = "image/jpeg"
|
||||||
|
b64 = base64.b64encode(buf.getvalue()).decode("ascii")
|
||||||
|
return f"data:{mime};base64,{b64}"
|
||||||
|
except Exception as e:
|
||||||
|
print(f" 警告: logo 处理失败: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- 模板渲染 ----------
|
||||||
|
|
||||||
|
def inline_css():
|
||||||
|
"""读取主题 CSS。"""
|
||||||
|
css_path = THEMES_DIR / "report.css"
|
||||||
|
if css_path.exists():
|
||||||
|
return css_path.read_text(encoding="utf-8")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def render_template(config, content, toc, logo_data_uri=None):
|
||||||
|
env = Environment(
|
||||||
|
loader=FileSystemLoader(TEMPLATES_DIR),
|
||||||
|
autoescape=select_autoescape(["html", "xml"]),
|
||||||
|
)
|
||||||
|
template = env.get_template("report.html")
|
||||||
|
css = inline_css()
|
||||||
|
return template.render(
|
||||||
|
title=config["title"],
|
||||||
|
subtitle=config["subtitle"],
|
||||||
|
author=config["author"],
|
||||||
|
version=config["version"],
|
||||||
|
doc_type=config["doc_type"],
|
||||||
|
logo=logo_data_uri,
|
||||||
|
date=config["date"],
|
||||||
|
content=content,
|
||||||
|
toc=toc,
|
||||||
|
css=css,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- 检查脚本 ----------
|
||||||
|
|
||||||
|
def run_checks(checks):
|
||||||
|
issues = []
|
||||||
|
for name, module in checks.items():
|
||||||
|
func = getattr(module, "check", None)
|
||||||
|
if not callable(func):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
result = func(str(PROJECT_ROOT))
|
||||||
|
if result:
|
||||||
|
issues.extend(result)
|
||||||
|
except Exception as e:
|
||||||
|
print(f" 警告: 检查脚本 {name} 执行失败: {e}")
|
||||||
|
if issues:
|
||||||
|
print("检查发现问题:")
|
||||||
|
for item in issues:
|
||||||
|
print(f" - {item}")
|
||||||
|
|
||||||
|
|
||||||
# ---------- 主入口 ----------
|
# ---------- 主入口 ----------
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
print("=== ez-Q 2.5 读出子系统编程控制模型 构建 ===")
|
print("=== Docs as Code 构建 ===")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
# 1. 加载配置
|
# 0. 加载配置
|
||||||
print("[1/3] 读取 project.yaml ...")
|
config = load_config()
|
||||||
config = load_project_config()
|
print(f"[配置] {config['title']} — {config['version']}")
|
||||||
print(f" 项目: {config.get('title', '未命名')}")
|
|
||||||
print(f" 版本: {config.get('version', 'N/A')}")
|
|
||||||
print(f" 章节数: {len(config.get('chapters', []))}")
|
print(f" 章节数: {len(config.get('chapters', []))}")
|
||||||
|
|
||||||
# 2. 拼接章节
|
# 0a. 加载插件
|
||||||
print("[2/3] 拼接章节 ...")
|
print("[插件] 加载扩展模块 ...")
|
||||||
assembled = read_and_assemble(config)
|
renderers = load_plugin_modules(RENDERERS_DIR)
|
||||||
print(f" 总字符数: {len(assembled)}")
|
checks = load_plugin_modules(CHECKS_DIR)
|
||||||
|
|
||||||
# 3. 生成 HTML
|
# 0b. 运行检查
|
||||||
print("[3/3] 生成 HTML ...")
|
if config.get("checks", True) and checks:
|
||||||
html = generate_html(assembled, config)
|
print("[检查] 运行检查脚本 ...")
|
||||||
|
run_checks(checks)
|
||||||
|
|
||||||
# 4. 输出
|
# 1. 逐章节处理
|
||||||
|
chapters = config.get("chapters", [])
|
||||||
|
if not chapters:
|
||||||
|
sys.exit("错误: project.yaml 中未定义 chapters 列表")
|
||||||
|
|
||||||
|
html_parts = []
|
||||||
|
for ch_file in chapters:
|
||||||
|
ch_path = CHAPTERS_DIR / ch_file
|
||||||
|
if not ch_path.exists():
|
||||||
|
print(f" 警告: 章节文件不存在,跳过: {ch_file}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
text = ch_path.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
# @import 处理
|
||||||
|
text = process_imports(text, ch_path.parent, renderers)
|
||||||
|
|
||||||
|
# 图片属性处理
|
||||||
|
text = process_image_attrs(text)
|
||||||
|
|
||||||
|
# KaTeX 预处理
|
||||||
|
text = preprocess_katex(text)
|
||||||
|
|
||||||
|
# 自定义代码块渲染
|
||||||
|
text = render_custom_codeblocks_md(text, renderers)
|
||||||
|
|
||||||
|
# Markdown → HTML
|
||||||
|
chapter_html = markdown_to_html(text)
|
||||||
|
|
||||||
|
# 图片 base64 内嵌
|
||||||
|
base_dirs = [ch_path.parent, ASSETS_DIR, PROJECT_ROOT]
|
||||||
|
chapter_html = embed_images(chapter_html, base_dirs)
|
||||||
|
|
||||||
|
html_parts.append(chapter_html)
|
||||||
|
|
||||||
|
full_html = "\n\n".join(html_parts)
|
||||||
|
print(f" HTML 总字符数: {len(full_html)}")
|
||||||
|
|
||||||
|
# 2. 注入锚点 + 提取目录
|
||||||
|
full_html = inject_anchors(full_html)
|
||||||
|
toc = generate_toc(full_html)
|
||||||
|
print(f" 目录条目数: {len(toc)}")
|
||||||
|
|
||||||
|
# 3. Logo 处理
|
||||||
|
logo_path = find_logo(config.get("logo", ""))
|
||||||
|
logo_data_uri = embed_logo(logo_path) if logo_path else None
|
||||||
|
if logo_data_uri:
|
||||||
|
print(" logo: 已内嵌")
|
||||||
|
else:
|
||||||
|
print(" logo: 未配置,封面将不显示 logo")
|
||||||
|
|
||||||
|
# 4. 模板渲染 + 输出
|
||||||
|
output_filename = re.sub(r'[^\w\-.]', '_', config["title"]) + ".html"
|
||||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
output_path = OUTPUT_DIR / OUTPUT_NAME
|
output_path = OUTPUT_DIR / output_filename
|
||||||
output_path.write_text(html, encoding="utf-8")
|
|
||||||
print(f" 输出: {output_path}")
|
|
||||||
print(f" 文件大小: {output_path.stat().st_size:,} 字节")
|
|
||||||
|
|
||||||
print()
|
final_html = render_template(config, full_html, toc, logo_data_uri)
|
||||||
print("Build completed successfully!")
|
output_path.write_text(final_html, encoding="utf-8")
|
||||||
|
print(f"\n构建完成:{output_path}")
|
||||||
|
print(f"文件大小: {output_path.stat().st_size:,} 字节")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
|
|
@ -1,65 +1,55 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta name="author" content="{{ author }}">
|
<title>{{ title }}</title>
|
||||||
<title>{{ title }} — {{ version }}</title>
|
|
||||||
|
|
||||||
{# KaTeX 数学公式支持 #}
|
{# KaTeX 数学公式支持 #}
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.25/dist/katex.min.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.25/dist/katex.min.css">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.25/dist/katex.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.25/dist/katex.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.25/dist/contrib/auto-render.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.25/dist/contrib/auto-render.min.js"></script>
|
||||||
|
|
||||||
<style>
|
<style>{{ css | safe }}</style>
|
||||||
{{ css_screen }}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
{# 打印样式 #}
|
|
||||||
<style media="print">
|
|
||||||
{{ css_print }}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div class="layout">
|
||||||
<header class="report-header">
|
<nav class="sidebar" id="toc-sidebar">
|
||||||
<h1 class="report-title">{{ title }}</h1>
|
<h2>目 录</h2>
|
||||||
{% if subtitle %}
|
|
||||||
<p class="report-subtitle">{{ subtitle }}</p>
|
|
||||||
{% endif %}
|
|
||||||
<div class="report-meta">
|
|
||||||
<span>版本: {{ version }}</span>
|
|
||||||
<span>作者: {{ author }}</span>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{# 侧边栏目录 #}
|
|
||||||
<nav class="sidebar-toc">
|
|
||||||
<h2>目录</h2>
|
|
||||||
<ul>
|
<ul>
|
||||||
{% for item in toc %}
|
{% for item in toc %}
|
||||||
{% if item.level == 1 %}
|
<li class="toc-level-{{ item.level }}">
|
||||||
<li class="toc-h1"><a href="#{{ item.anchor }}">{{ item.title }}</a></li>
|
<a href="#{{ item.anchor }}">{{ item.text }}</a>
|
||||||
{% elif item.level == 2 %}
|
</li>
|
||||||
<li class="toc-h2"><a href="#{{ item.anchor }}">{{ item.title }}</a></li>
|
|
||||||
{% elif item.level == 3 %}
|
|
||||||
<li class="toc-h3"><a href="#{{ item.anchor }}">{{ item.title }}</a></li>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{# 正文内容 #}
|
<div class="main">
|
||||||
<main class="report-body">
|
<div class="cover">
|
||||||
{{ body }}
|
{% if logo %}
|
||||||
</main>
|
<img class="cover-logo" src="{{ logo }}" alt="logo">
|
||||||
|
{% endif %}
|
||||||
|
<div class="cover-doc-type">{{ doc_type }}</div>
|
||||||
|
<h1 class="cover-title">{{ title }}</h1>
|
||||||
|
{% if subtitle %}<p class="cover-subtitle">{{ subtitle }}</p>{% endif %}
|
||||||
|
<div class="cover-meta">
|
||||||
|
<table>
|
||||||
|
{% if author %}<tr><td>作者</td><td>{{ author }}</td></tr>{% endif %}
|
||||||
|
{% if version %}<tr><td>版本</td><td>{{ version }}</td></tr>{% endif %}
|
||||||
|
<tr><td>日期</td><td>{{ date }}</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<footer class="report-footer">
|
<main class="content">
|
||||||
<p>{{ title }} — {{ version }} — {{ author }}</p>
|
{{ content | safe }}
|
||||||
</footer>
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{# KaTeX 自动渲染 #}
|
{# KaTeX 自动渲染 #}
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
renderMathInElement(document.body, {
|
renderMathInElement(document.body, {
|
||||||
delimiters: [
|
delimiters: [
|
||||||
|
|
@ -68,7 +58,6 @@
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
/* ============================================
|
|
||||||
ez-Q 2.5 读出子系统编程控制模型 — 打印样式
|
|
||||||
============================================ */
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
body {
|
|
||||||
font-size: 12pt;
|
|
||||||
color: #000;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-toc {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.report-header {
|
|
||||||
background: none;
|
|
||||||
color: #000;
|
|
||||||
padding: 1em 0;
|
|
||||||
border-bottom: 2px solid #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.report-body {
|
|
||||||
max-width: 100%;
|
|
||||||
padding: 1em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.report-body h1 {
|
|
||||||
page-break-before: always;
|
|
||||||
font-size: 16pt;
|
|
||||||
}
|
|
||||||
|
|
||||||
.report-body h2 {
|
|
||||||
font-size: 14pt;
|
|
||||||
}
|
|
||||||
|
|
||||||
.report-body h3 {
|
|
||||||
font-size: 12pt;
|
|
||||||
}
|
|
||||||
|
|
||||||
.report-body pre {
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
background: #f9f9f9;
|
|
||||||
page-break-inside: avoid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.report-body table {
|
|
||||||
page-break-inside: avoid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.report-body img {
|
|
||||||
max-width: 100%;
|
|
||||||
page-break-inside: avoid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.report-footer {
|
|
||||||
border-top: 1px solid #000;
|
|
||||||
font-size: 10pt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,222 +1,280 @@
|
||||||
/* ============================================
|
/* ============================================
|
||||||
ez-Q 2.5 读出子系统编程控制模型 — 屏幕样式
|
Docs as Code — 屏幕与打印样式
|
||||||
|
严格对齐 docs-as-code skill 参考实现
|
||||||
============================================ */
|
============================================ */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--primary: #1a237e;
|
--primary: #0d47a1;
|
||||||
--accent: #283593;
|
--secondary: #1565c0;
|
||||||
--bg-light: #f5f5f5;
|
--accent: #42a5f5;
|
||||||
--bg-white: #ffffff;
|
--light: #e3f2fd;
|
||||||
--text: #333333;
|
--text: #212121;
|
||||||
--text-light: #5c5c5c;
|
--muted: #616161;
|
||||||
--border: #d6d6d6;
|
--bg: #f0f2f5;
|
||||||
--code-bg: #f0f0f0;
|
--sidebar-w: 260px;
|
||||||
--sidebar-width: 280px;
|
--content-w: 210mm;
|
||||||
|
--a4-h: 297mm;
|
||||||
|
--cover-px: 25mm;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0;
|
html { scroll-behavior: smooth; }
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Helvetica Neue', Helvetica, 'Segoe UI', Arial, 'Microsoft YaHei', sans-serif;
|
font-family: "Noto Sans SC", "Microsoft YaHei", "PingFang SC", sans-serif;
|
||||||
font-size: 16px;
|
font-size: 11pt;
|
||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
background-color: var(--bg-white);
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 整体布局:左侧目录 + 右侧主内容 ---- */
|
||||||
|
.layout {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
max-width: calc(var(--sidebar-w) + var(--content-w));
|
||||||
|
margin: 0 auto;
|
||||||
|
background: #fff;
|
||||||
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- 页头 ---- */
|
.sidebar {
|
||||||
.report-header {
|
|
||||||
width: 100%;
|
|
||||||
background: linear-gradient(135deg, var(--primary), var(--accent));
|
|
||||||
color: white;
|
|
||||||
padding: 2em 2em 1.5em;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.report-title {
|
|
||||||
font-size: 2em;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 0.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.report-subtitle {
|
|
||||||
font-size: 1.1em;
|
|
||||||
opacity: 0.85;
|
|
||||||
margin-bottom: 0.75em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.report-meta {
|
|
||||||
font-size: 0.9em;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.report-meta span {
|
|
||||||
margin: 0 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- 侧边栏 ---- */
|
|
||||||
.sidebar-toc {
|
|
||||||
width: var(--sidebar-width);
|
|
||||||
min-width: var(--sidebar-width);
|
|
||||||
padding: 1.5em 1em;
|
|
||||||
background: var(--bg-light);
|
|
||||||
border-right: 1px solid var(--border);
|
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
width: var(--sidebar-w);
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
font-size: 14px;
|
flex-shrink: 0;
|
||||||
|
background: #fafbfc;
|
||||||
|
border-right: 1px solid #e0e0e0;
|
||||||
|
padding: 24px 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-toc h2 {
|
.sidebar h2 {
|
||||||
font-size: 1.1em;
|
font-size: 13pt;
|
||||||
margin-bottom: 0.75em;
|
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-toc ul {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-toc li {
|
|
||||||
margin: 0.25em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-toc a {
|
|
||||||
color: var(--text);
|
|
||||||
text-decoration: none;
|
|
||||||
display: block;
|
|
||||||
padding: 0.15em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-toc a:hover {
|
|
||||||
color: var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.toc-h1 { font-weight: 600; margin-top: 0.5em; }
|
|
||||||
.toc-h2 { padding-left: 1em; }
|
|
||||||
.toc-h3 { padding-left: 2em; font-size: 0.95em; color: var(--text-light); }
|
|
||||||
|
|
||||||
/* ---- 正文 ---- */
|
|
||||||
.report-body {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
max-width: 900px;
|
|
||||||
padding: 2em 2.5em 4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.report-body h1 {
|
|
||||||
font-size: 1.8em;
|
|
||||||
font-weight: 600;
|
|
||||||
margin: 1.2em 0 0.6em;
|
|
||||||
padding-bottom: 0.3em;
|
|
||||||
border-bottom: 2px solid var(--primary);
|
border-bottom: 2px solid var(--primary);
|
||||||
color: var(--primary);
|
padding-bottom: 8px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.report-body h2 {
|
.sidebar ul { list-style: none; padding: 0; }
|
||||||
font-size: 1.4em;
|
|
||||||
font-weight: 600;
|
|
||||||
margin: 1em 0 0.5em;
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.report-body h3 {
|
.sidebar li {
|
||||||
font-size: 1.15em;
|
padding: 4px 0;
|
||||||
font-weight: 600;
|
font-size: 9.5pt;
|
||||||
margin: 0.8em 0 0.4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.report-body p {
|
|
||||||
margin: 0.5em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.report-body img {
|
|
||||||
max-width: 100%;
|
|
||||||
display: block;
|
|
||||||
margin: 1em auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- 表格 ---- */
|
|
||||||
.report-body table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
margin: 0.75em 0 1.25em;
|
|
||||||
font-size: 0.95em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.report-body th,
|
|
||||||
.report-body td {
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
padding: 6px 12px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.report-body th {
|
|
||||||
background: var(--bg-light);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- 代码块 ---- */
|
|
||||||
.report-body code {
|
|
||||||
font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
|
||||||
font-size: 0.9em;
|
|
||||||
background: var(--code-bg);
|
|
||||||
padding: 0.15em 0.35em;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.report-body pre {
|
|
||||||
background: var(--bg-light);
|
|
||||||
padding: 1em 1.2em;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
overflow-x: auto;
|
|
||||||
margin: 0.75em 0;
|
|
||||||
font-size: 0.9em;
|
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.report-body pre code {
|
.sidebar li a {
|
||||||
background: none;
|
color: #555;
|
||||||
padding: 0;
|
text-decoration: none;
|
||||||
|
display: block;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
transition: all 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- 引用块 ---- */
|
.sidebar li a:hover {
|
||||||
.report-body blockquote {
|
color: var(--primary);
|
||||||
margin: 0.75em 0;
|
background: var(--light);
|
||||||
padding: 0.5em 1em;
|
|
||||||
background: var(--bg-light);
|
|
||||||
border-left: 4px solid var(--border);
|
|
||||||
color: var(--text-light);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- 列表 ---- */
|
.sidebar .toc-level-2 { padding-left: 12px; }
|
||||||
.report-body ul,
|
.sidebar .toc-level-3 { padding-left: 24px; }
|
||||||
.report-body ol {
|
.sidebar .toc-level-4 { padding-left: 36px; }
|
||||||
margin: 0.5em 0;
|
|
||||||
padding-left: 2em;
|
.main {
|
||||||
|
flex: 1;
|
||||||
|
max-width: var(--content-w);
|
||||||
|
min-width: 0;
|
||||||
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.report-body li {
|
/* ---- 封面(A4) ---- */
|
||||||
margin: 0.15em 0;
|
.cover {
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- 页脚 ---- */
|
|
||||||
.report-footer {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: var(--a4-h);
|
||||||
|
min-height: var(--a4-h);
|
||||||
|
padding: 35mm var(--cover-px) 50mm;
|
||||||
|
background: #fff;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 1.5em;
|
page-break-after: always;
|
||||||
font-size: 0.85em;
|
border-bottom: 1px solid #e0e0e0;
|
||||||
color: var(--text-light);
|
}
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
|
.cover-logo {
|
||||||
|
max-width: 320px;
|
||||||
|
max-height: 90px;
|
||||||
|
height: auto;
|
||||||
|
width: auto;
|
||||||
|
margin: 0;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-doc-type {
|
||||||
|
font-size: 13pt;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--secondary);
|
||||||
|
letter-spacing: 0.5em;
|
||||||
|
text-indent: 0.5em;
|
||||||
|
margin-top: 15mm;
|
||||||
|
margin-bottom: 15mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-title {
|
||||||
|
font-family: "Noto Sans SC", "Microsoft YaHei", "PingFang SC", sans-serif;
|
||||||
|
font-size: 26pt;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
background: var(--primary);
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0 calc(-1 * var(--cover-px)) 0;
|
||||||
|
padding: 0.4em var(--cover-px);
|
||||||
|
width: calc(100% + 2 * var(--cover-px));
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-subtitle {
|
||||||
|
font-size: 16pt;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--secondary);
|
||||||
|
margin-top: 8mm;
|
||||||
|
margin-bottom: 18mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-meta {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 130mm;
|
||||||
|
margin-top: auto;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-meta table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 11pt;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-meta td {
|
||||||
|
padding: 2.5mm 4mm;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-meta td:first-child {
|
||||||
|
text-align: center;
|
||||||
|
color: #9e9e9e;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-meta td:last-child {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 正文 ---- */
|
||||||
|
.content {
|
||||||
|
padding: 2cm 2.5cm 3cm;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 正文排版 ---- */
|
||||||
|
h1 {
|
||||||
|
font-family: "Noto Serif SC", "SimSun", serif;
|
||||||
|
font-size: 18pt;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--primary);
|
||||||
|
border-bottom: 2px solid var(--primary);
|
||||||
|
padding-bottom: 0.3em;
|
||||||
|
margin-top: 1.8em;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-family: "Noto Serif SC", "SimSun", serif;
|
||||||
|
font-size: 14pt;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--secondary);
|
||||||
|
border-bottom: 1px solid var(--light);
|
||||||
|
padding-bottom: 0.2em;
|
||||||
|
margin-top: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 12pt;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303f9f;
|
||||||
|
margin-top: 1.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 { font-size: 11pt; font-weight: 600; }
|
||||||
|
|
||||||
|
p { margin: 0.6em 0; }
|
||||||
|
|
||||||
|
strong { color: var(--primary); }
|
||||||
|
|
||||||
|
ul, ol { padding-left: 2em; margin: 0.6em 0; }
|
||||||
|
li { margin: 0.25em 0; }
|
||||||
|
|
||||||
|
/* ---- 表格 ---- */
|
||||||
|
table {
|
||||||
|
font-size: 10pt;
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background: var(--primary);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:nth-child(even) { background: #f8f9fa; }
|
||||||
|
|
||||||
|
/* ---- 代码 ---- */
|
||||||
|
pre {
|
||||||
|
background: #f5f5f5;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: "JetBrains Mono", "Cascadia Code", "Consolas", monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
background: #f0f0f0;
|
||||||
|
padding: 1px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre code { background: none; padding: 0; }
|
||||||
|
|
||||||
|
/* ---- 图片(无阴影) ---- */
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
margin: 1em auto;
|
||||||
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- KaTeX 公式 ---- */
|
/* ---- KaTeX 公式 ---- */
|
||||||
|
|
@ -226,21 +284,45 @@ body {
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- 移动端响应式 ---- */
|
/* ---- 响应式 ---- */
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 1100px) {
|
||||||
.sidebar-toc {
|
:root { --cover-px: 15mm; }
|
||||||
display: none;
|
.layout { flex-direction: column; }
|
||||||
|
.sidebar {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
}
|
}
|
||||||
|
.main { max-width: none; }
|
||||||
.report-body {
|
.content { padding: 1.5cm; }
|
||||||
padding: 1em 1.2em 3em;
|
.cover {
|
||||||
|
width: calc(100% - 2rem);
|
||||||
|
height: auto;
|
||||||
|
min-height: auto;
|
||||||
|
padding: 15mm;
|
||||||
|
margin: 1rem auto;
|
||||||
}
|
}
|
||||||
|
.cover-title { font-size: 22pt; }
|
||||||
|
.cover-subtitle { font-size: 14pt; }
|
||||||
|
}
|
||||||
|
|
||||||
.report-header {
|
/* ---- 打印 ---- */
|
||||||
padding: 1.5em 1em 1em;
|
@media print {
|
||||||
|
body { background: #fff; }
|
||||||
|
.layout { display: block; max-width: none; }
|
||||||
|
.sidebar { display: none; }
|
||||||
|
.main { max-width: none; }
|
||||||
|
.cover {
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
min-height: 100vh;
|
||||||
|
page-break-after: always;
|
||||||
}
|
}
|
||||||
|
.content {
|
||||||
.report-title {
|
max-width: none;
|
||||||
font-size: 1.5em;
|
padding: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@
|
||||||
title: 读出子系统历史无关功能配置项
|
title: 读出子系统历史无关功能配置项
|
||||||
subtitle: ez-Q 2.5 读出子系统编程控制模型
|
subtitle: ez-Q 2.5 读出子系统编程控制模型
|
||||||
author: 郭成
|
author: 郭成
|
||||||
version: V0.2
|
version: V0.3
|
||||||
|
doc_type: 用户手册
|
||||||
|
logo: assets/logo.png # 封面 logo 路径(需明确指定;不指定则无 logo)
|
||||||
lang: zh-CN
|
lang: zh-CN
|
||||||
|
|
||||||
chapters:
|
chapters:
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,7 @@
|
||||||
# 安装: pip install -r requirements.txt
|
# 安装: pip install -r requirements.txt
|
||||||
PyYAML>=6.0
|
PyYAML>=6.0
|
||||||
markdown>=3.5
|
markdown>=3.5
|
||||||
|
pymdown-extensions>=10.0
|
||||||
Jinja2>=3.1
|
Jinja2>=3.1
|
||||||
|
Pygments>=2.16
|
||||||
|
Pillow>=10.0
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue