格式修复

This commit is contained in:
guocheng 2026-07-26 00:00:24 +08:00
parent 68acdb1aa3
commit 5b663f4014
14 changed files with 892 additions and 485 deletions

7
.gitignore vendored
View File

@ -1,2 +1,7 @@
# Build output # Build output
output/ output/
# Python
__pycache__/
*.pyc
*.pyo

View File

@ -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**,确保拼接后层级正确
### 图片规范 ### 图片规范
- **一律使用项目根目录相对路径**: `![描述](./assets/xxx.png)` - **从 chapters/ 引用项目根目录 assets/**: `![描述](../assets/xxx.png)`
- 此路径同时兼容标准 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 注入当前章节
- `![描述](path){w=50%}` — 图片属性控制(预留) - `@import "../data/file.csv" using render_custom` — 使用 `doc_builder/renderers/render_custom.py` 渲染
- 自定义代码块渲染器(预留 `doc_builder/renderers/` - `![描述](../assets/x.png){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 兼容文件

View File

@ -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`** — 使用自定义渲染器
- **`![alt](../assets/x.png){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` 进行构建。

BIN
assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 686 KiB

View File

@ -56,7 +56,7 @@ ez-Q 2.5 ASIC平台读出子系统由读出基带板、读出混频板和读出
- 读出混频板自身产生一个本振信号,用于实现基带信号与射频信号之间的转换; - 读出混频板自身产生一个本振信号,用于实现基带信号与射频信号之间的转换;
- 而读出泵浦板负责产生一个指定功率频率的单音信号,并在外部使能信号控制下输出; - 而读出泵浦板负责产生一个指定功率频率的单音信号,并在外部使能信号控制下输出;
![读出子系统组成](./assets/readout_system.png) ![读出子系统组成](../assets/readout_system.png)
当软件需要对不同通道编程时其通过ip地址指定机箱、通过槽位号指定板卡、 当软件需要对不同通道编程时其通过ip地址指定机箱、通过槽位号指定板卡、
通过扩展地址指定同一个板卡内的多个通道、通过地址指定一个通道内的不同配置项。 通过扩展地址指定同一个板卡内的多个通道、通过地址指定一个通道内的不同配置项。

View File

@ -15,7 +15,7 @@
读出系统的的读出参数存储格式如下图所示。 读出系统的的读出参数存储格式如下图所示。
FPGA平台下的系数直读模式只使用`Ctrl`部分中的数据,并忽略`dds_pfw`控制字. FPGA平台下的系数直读模式只使用`Ctrl`部分中的数据,并忽略`dds_pfw`控制字.
![读出ACQ通道控制模型](./assets/readout_para.png) ![读出ACQ通道控制模型](../assets/readout_para.png)
### 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*32KBQ路数据偏移地址为x*32KB+16 KB其中x为Qubit序号范围为0~15。 - I路数据偏移地址为x*32KBQ路数据偏移地址为x*32KB+16 KB其中x为Qubit序号范围为0~15。
![读出ACQ通道控制模型](./assets/readout_mtf.png) ![读出ACQ通道控制模型](../assets/readout_mtf.png)

View File

@ -6,4 +6,4 @@
索引表格式定义如下图所示: wave_id是mcu产生的码字其可以作为地址索引波形控制参数。 索引表格式定义如下图所示: wave_id是mcu产生的码字其可以作为地址索引波形控制参数。
波形控制参数包括波形地址`addr`和波形长度`len`参数,颗粒度是时钟周期。 波形控制参数包括波形地址`addr`和波形长度`len`参数,颗粒度是时钟周期。
![波形查找表和波形仓库](./assets/readout_lut.png) ![波形查找表和波形仓库](../assets/readout_lut.png)

View File

@ -9,7 +9,7 @@ MCU的指令、MCU的数据、控制寄存器、波形索性表、波形仓库
当前模拟电路仅需配置Pump参数下图是EXC-Pump通道数字部分的编程控制模型。 当前模拟电路仅需配置Pump参数下图是EXC-Pump通道数字部分的编程控制模型。
![读出RI-Pump通道控制模型](./assets/readout_ri.png) ![读出RI-Pump通道控制模型](../assets/readout_ri.png)
1. EXC-Pump通道的波形输出由AWG模块MCU发出的码字触发。 1. EXC-Pump通道的波形输出由AWG模块MCU发出的码字触发。
对于输出波形而言MCU发出的码字定义波形的索引ID 对于输出波形而言MCU发出的码字定义波形的索引ID
@ -29,7 +29,7 @@ MCU的指令、MCU的数据、控制寄存器、波形索性表、波形仓库
最后数字信号经过DAC转换成基带信号基带信号再和外部本振信号模拟混频后输出读出激励波形。 最后数字信号经过DAC转换成基带信号基带信号再和外部本振信号模拟混频后输出读出激励波形。
读出芯片不同模式输出的频响曲线如下图所示: 读出芯片不同模式输出的频响曲线如下图所示:
![output_response](./assets/output_response.png) ![output_response](../assets/output_response.png)
为了兼容混频输出和射频直出两种工作模式以及在FPGA和ASIC平台上实现半带滤波器和MIX模块都支持旁路功能因此最终波形输出支持NRZ、MIX、HBNRZ和HBMIX四种模式。 为了兼容混频输出和射频直出两种工作模式以及在FPGA和ASIC平台上实现半带滤波器和MIX模块都支持旁路功能因此最终波形输出支持NRZ、MIX、HBNRZ和HBMIX四种模式。

View File

@ -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'![{alt}]({abs_path})'
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):
"""处理 ![](path){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"![{alt_text}]({data_uri})" return match.group(0)
else:
return f"![{alt_text}](assets/{img_path})"
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__":

View File

@ -1,74 +1,63 @@
<!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">
<nav class="sidebar" id="toc-sidebar">
<h2>目 录</h2>
<ul>
{% for item in toc %}
<li class="toc-level-{{ item.level }}">
<a href="#{{ item.anchor }}">{{ item.text }}</a>
</li>
{% endfor %}
</ul>
</nav>
<header class="report-header"> <div class="main">
<h1 class="report-title">{{ title }}</h1> <div class="cover">
{% if subtitle %} {% if logo %}
<p class="report-subtitle">{{ subtitle }}</p> <img class="cover-logo" src="{{ logo }}" alt="logo">
{% endif %} {% endif %}
<div class="report-meta"> <div class="cover-doc-type">{{ doc_type }}</div>
<span>版本: {{ version }}</span> <h1 class="cover-title">{{ title }}</h1>
<span>作者: {{ author }}</span> {% 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>
<main class="content">
{{ content | safe }}
</main>
</div>
</div> </div>
</header>
{# 侧边栏目录 #} {# KaTeX 自动渲染 #}
<nav class="sidebar-toc"> <script>
<h2>目录</h2> document.addEventListener("DOMContentLoaded", function () {
<ul> renderMathInElement(document.body, {
{% for item in toc %} delimiters: [
{% if item.level == 1 %} {left: "$$", right: "$$", display: true},
<li class="toc-h1"><a href="#{{ item.anchor }}">{{ item.title }}</a></li> {left: "$", right: "$", display: false}
{% elif item.level == 2 %} ]
<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 %}
</ul>
</nav>
{# 正文内容 #}
<main class="report-body">
{{ body }}
</main>
<footer class="report-footer">
<p>{{ title }} — {{ version }} — {{ author }}</p>
</footer>
{# KaTeX 自动渲染 #}
<script>
document.addEventListener("DOMContentLoaded", function () {
renderMathInElement(document.body, {
delimiters: [
{left: "$$", right: "$$", display: true},
{left: "$", right: "$", display: false}
]
}); });
}); </script>
</script>
</body> </body>
</html> </html>

View File

@ -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;
}
}

View File

@ -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;
} }
} }

View File

@ -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:

View File

@ -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