refactor: 重构文档结构,采用渐进式信息披露模式
将 README.md 拆分为多个专题文档,减少认知负荷: - 用户文档迁移到 docs/ (用户指南、元素、模板、参考等) - 开发文档迁移到 docs/development/ (架构、模块、规范) - README.md 精简至 ~290 行,仅保留概览和导航 - 删除 README_DEV.md,内容已迁移 - 归档 OpenSpec 变更 refactor-docs-progressive-disclosure
This commit is contained in:
280
docs/development/extending.md
Normal file
280
docs/development/extending.md
Normal file
@@ -0,0 +1,280 @@
|
||||
# 扩展指南
|
||||
|
||||
本文档说明如何扩展 yaml2pptx 的功能。
|
||||
|
||||
## 添加新元素类型
|
||||
|
||||
假设要添加 `VideoElement`:
|
||||
|
||||
### 1. 在 core/elements.py 中定义数据类
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
@dataclass
|
||||
class VideoElement:
|
||||
type: str = 'video'
|
||||
src: str = ''
|
||||
box: list = field(default_factory=lambda: [1, 1, 4, 3])
|
||||
autoplay: bool = False
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.src:
|
||||
raise ValueError("视频元素必须指定 src")
|
||||
if len(self.box) != 4:
|
||||
raise ValueError("box 必须包含 4 个数字")
|
||||
```
|
||||
|
||||
### 2. 在工厂函数中添加分支
|
||||
|
||||
```python
|
||||
def create_element(elem_dict: dict):
|
||||
elem_type = elem_dict.get('type')
|
||||
|
||||
if elem_type == 'text':
|
||||
return TextElement(**elem_dict)
|
||||
elif elem_type == 'image':
|
||||
return ImageElement(**elem_dict)
|
||||
# ... 其他类型 ...
|
||||
elif elem_type == 'video':
|
||||
return VideoElement(**elem_dict)
|
||||
else:
|
||||
raise ValueError(f"Unknown element type: {elem_type}")
|
||||
```
|
||||
|
||||
### 3. 在 PptxGenerator 中实现渲染方法
|
||||
|
||||
```python
|
||||
def _render_element(self, slide, elem, base_path):
|
||||
# ... 其他类型 ...
|
||||
elif isinstance(elem, VideoElement):
|
||||
self._render_video(slide, elem, base_path)
|
||||
|
||||
def _render_video(self, slide, elem: VideoElement, base_path):
|
||||
"""实现视频渲染逻辑"""
|
||||
movie = slide.shapes.add_movie(
|
||||
str(Path(base_path) / elem.src),
|
||||
left=Inches(elem.box[0]),
|
||||
top=Inches(elem.box[1]),
|
||||
width=Inches(elem.box[2]),
|
||||
height=Inches(elem.box[3])
|
||||
)
|
||||
|
||||
if elem.autoplay:
|
||||
movie.click.action = pp.action.Action(pyppote.xmlns.namespace('p').MSO_ANIMATION_VIDEO_CLICK)
|
||||
```
|
||||
|
||||
### 4. 在 HtmlRenderer 中实现渲染方法
|
||||
|
||||
```python
|
||||
def render_slide(self, slide_data, index, base_path=None):
|
||||
elements_html = ""
|
||||
|
||||
for elem in slide_data:
|
||||
# ... 其他类型 ...
|
||||
elif isinstance(elem, VideoElement):
|
||||
elements_html += self.render_video(elem, base_path)
|
||||
|
||||
return self.SLIDE_TEMPLATE.format(content=elements_html)
|
||||
|
||||
def render_video(self, elem: VideoElement, base_path):
|
||||
"""实现 HTML 视频渲染"""
|
||||
src_path = str(Path(base_path) / elem.src) if base_path else elem.src
|
||||
autoplay_attr = "autoplay" if elem.autoplay else ""
|
||||
|
||||
return f'''
|
||||
<video src="{src_path}" {autoplay_attr}
|
||||
style="position:absolute; left:{elem.box[0]*96}px; top:{elem.box[1]*96}px;
|
||||
width:{elem.box[2]*96}px; height:{elem.box[3]*96}px;">
|
||||
</video>
|
||||
'''
|
||||
```
|
||||
|
||||
### 5. 更新验证器
|
||||
|
||||
如果需要验证视频文件:
|
||||
|
||||
```python
|
||||
# validators/resource.py
|
||||
def validate_video(self, src, slide_index, elem_index):
|
||||
"""检查视频文件是否存在"""
|
||||
video_path = self.base_dir / src
|
||||
if not video_path.exists():
|
||||
return ValidationIssue(
|
||||
level="ERROR",
|
||||
message=f"视频文件不存在: {src}",
|
||||
location=f"[幻灯片 {slide_index + 1}, 元素 {elem_index + 1}]",
|
||||
code="VIDEO_FILE_NOT_FOUND"
|
||||
)
|
||||
return None
|
||||
```
|
||||
|
||||
## 添加新渲染器
|
||||
|
||||
假设要添加 PDF 渲染器:
|
||||
|
||||
### 1. 创建 renderers/pdf_renderer.py
|
||||
|
||||
```python
|
||||
from core.elements import TextElement, ImageElement, ShapeElement, TableElement
|
||||
|
||||
class PdfRenderer:
|
||||
def __init__(self, size="16:9"):
|
||||
# 初始化 PDF 库
|
||||
self.size = size
|
||||
# ...
|
||||
|
||||
def add_slide(self, slide_data, base_path=None):
|
||||
"""添加页面"""
|
||||
# 实现...
|
||||
pass
|
||||
|
||||
def _render_element(self, page, elem, base_path):
|
||||
"""渲染元素到 PDF 页面"""
|
||||
if isinstance(elem, TextElement):
|
||||
self._render_text(page, elem)
|
||||
elif isinstance(elem, ImageElement):
|
||||
self._render_image(page, elem, base_path)
|
||||
elif isinstance(elem, ShapeElement):
|
||||
self._render_shape(page, elem)
|
||||
elif isinstance(elem, TableElement):
|
||||
self._render_table(page, elem)
|
||||
|
||||
def _render_text(self, page, elem):
|
||||
"""渲染文本到 PDF"""
|
||||
# 实现...
|
||||
pass
|
||||
|
||||
def _render_image(self, page, elem, base_path):
|
||||
"""渲染图片到 PDF"""
|
||||
# 实现...
|
||||
pass
|
||||
|
||||
def _render_shape(self, page, elem):
|
||||
"""渲染形状到 PDF"""
|
||||
# 实现...
|
||||
pass
|
||||
|
||||
def _render_table(self, page, elem):
|
||||
"""渲染表格到 PDF"""
|
||||
# 实现...
|
||||
pass
|
||||
|
||||
def save(self, output_path):
|
||||
"""保存 PDF 文件"""
|
||||
# 实现...
|
||||
pass
|
||||
```
|
||||
|
||||
### 2. 在 yaml2pptx.py 中添加 PDF 模式
|
||||
|
||||
```python
|
||||
from renderers.pdf_renderer import PdfRenderer
|
||||
|
||||
def main():
|
||||
# ... 解析参数 ...
|
||||
if args.pdf:
|
||||
# PDF 生成模式
|
||||
generator = PdfRenderer(size=args.size)
|
||||
# ... 渲染逻辑
|
||||
```
|
||||
|
||||
### 3. 添加命令行参数
|
||||
|
||||
```python
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description='YAML to PPTX converter')
|
||||
subparsers = parser.add_subparsers(dest='command', help='子命令')
|
||||
|
||||
# ... 其他命令 ...
|
||||
|
||||
# PDF 命令
|
||||
pdf_parser = subparsers.add_parser('pdf', help='生成 PDF')
|
||||
pdf_parser.add_argument('input', help='输入的 YAML 文件')
|
||||
pdf_parser.add_argument('output', help='输出的 PDF 文件', nargs='?')
|
||||
pdf_parser.add_argument('--template', help='模板库文件路径')
|
||||
pdf_parser.add_argument('--size', default='16:9', choices=['16:9', '4:3'])
|
||||
|
||||
return parser.parse_args()
|
||||
```
|
||||
|
||||
## 添加新的验证规则
|
||||
|
||||
### 1. 在 validators/ 中创建新的验证器
|
||||
|
||||
```python
|
||||
# validators/custom.py
|
||||
|
||||
class CustomValidator:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def validate(self, presentation):
|
||||
"""执行自定义验证"""
|
||||
issues = []
|
||||
# 验证逻辑
|
||||
return issues
|
||||
```
|
||||
|
||||
### 2. 在主验证器中集成
|
||||
|
||||
```python
|
||||
# validators/validator.py
|
||||
|
||||
class Validator:
|
||||
def __init__(self, ...):
|
||||
# ...
|
||||
self.custom_validator = CustomValidator()
|
||||
|
||||
def validate_presentation(self, presentation):
|
||||
# ...
|
||||
# 调用自定义验证器
|
||||
custom_issues = self.custom_validator.validate(presentation)
|
||||
result.infos.extend(custom_issues)
|
||||
```
|
||||
|
||||
## 测试新功能
|
||||
|
||||
### 1. 创建测试文件
|
||||
|
||||
```python
|
||||
# tests/unit/test_video_element.py
|
||||
|
||||
import pytest
|
||||
from core.elements import VideoElement, create_element
|
||||
|
||||
def test_create_video_element():
|
||||
elem_dict = {
|
||||
'type': 'video',
|
||||
'src': 'test.mp4',
|
||||
'box': [1, 1, 4, 3],
|
||||
'autoplay': True
|
||||
}
|
||||
elem = create_element(elem_dict)
|
||||
assert isinstance(elem, VideoElement)
|
||||
assert elem.autoplay is True
|
||||
|
||||
def test_video_element_without_src():
|
||||
with pytest.raises(ValueError, match="必须指定 src"):
|
||||
VideoElement(src='', box=[1, 1, 4, 3])
|
||||
```
|
||||
|
||||
### 2. 运行测试
|
||||
|
||||
```bash
|
||||
uv run pytest tests/unit/test_video_element.py -v
|
||||
```
|
||||
|
||||
## 提交变更
|
||||
|
||||
1. 更新相关文档
|
||||
2. 添加测试
|
||||
3. 运行完整测试套件
|
||||
4. 提交 Pull Request
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [架构设计](architecture.md) - 代码结构
|
||||
- [Elements 模块](modules/elements.md) - 元素抽象层
|
||||
|
||||
[返回开发文档索引](../README.md)
|
||||
Reference in New Issue
Block a user