1
0

refactor: modularize yaml2pptx into layered architecture

Refactor yaml2pptx.py from a 1,245-line monolithic script into a modular
architecture with clear separation of concerns. The entry point is now
127 lines, with business logic distributed across focused modules.

Architecture:
- core/: Domain models (elements, template, presentation)
- loaders/: YAML loading and validation
- renderers/: PPTX and HTML rendering
- preview/: Flask preview server
- utils.py: Shared utilities

Key improvements:
- Element abstraction layer using dataclass with validation
- Renderer logic built into generator classes
- Single-direction dependencies (no circular imports)
- Each module 150-300 lines for better readability
- Backward compatible CLI interface

Documentation:
- README.md: User-facing usage guide
- README_DEV.md: Developer documentation

OpenSpec:
- Archive refactor-yaml2pptx-modular change (63/70 tasks complete)
- Sync 5 delta specs to main specs (2 new + 3 updated)
This commit is contained in:
2026-03-02 16:43:45 +08:00
parent b2132dc06b
commit ed940f0690
31 changed files with 3142 additions and 1307 deletions

191
core/template.py Normal file
View File

@@ -0,0 +1,191 @@
"""
模板系统模块
管理可复用的幻灯片布局和变量解析。
"""
import re
from pathlib import Path
from loaders.yaml_loader import YAMLError, load_yaml_file, validate_template_yaml
class Template:
"""模板类,管理可复用的幻灯片布局"""
def __init__(self, template_file, templates_dir=None):
"""
初始化模板
Args:
template_file: 模板名称(纯文件名,不含路径)
templates_dir: 模板文件目录
"""
# 检查是否提供了 templates_dir
if templates_dir is None:
raise YAMLError(
f"未指定模板目录,无法加载模板: {template_file}\n"
f"请使用 --template-dir 参数指定模板目录"
)
# 验证模板名称(不能包含路径分隔符)
if '/' in template_file or '\\' in template_file:
raise YAMLError(
f"模板名称不能包含路径分隔符: {template_file}\n"
f"模板名称应该是纯文件名,如: 'title-slide'"
)
# 构建模板路径
template_path = Path(templates_dir) / f"{template_file}.yaml"
# 检查文件是否存在
if not template_path.exists():
raise YAMLError(
f"模板文件不存在: {template_file}\n"
f"查找位置: {templates_dir}\n"
f"期望文件: {template_path}\n"
f"提示: 请检查模板名称和模板目录是否正确"
)
# 加载并验证模板文件
self.data = load_yaml_file(template_path)
validate_template_yaml(self.data, str(template_path))
# 解析变量定义
self.vars_def = {}
for var in self.data.get('vars', []):
self.vars_def[var['name']] = var
# 元素列表
self.elements = self.data.get('elements', [])
def resolve_value(self, value, vars_values):
"""
解析单个值中的变量引用
Args:
value: 要解析的值
vars_values: 用户提供的变量值字典
Returns:
解析后的值
"""
if not isinstance(value, str):
return value
# 匹配 {xxx} 模式
pattern = r'\{([^}]+)\}'
def replacer(match):
expr = match.group(1)
# 模板变量: {title}
if expr in vars_values:
return str(vars_values[expr])
else:
raise YAMLError(f"未定义的变量: {expr}")
result = re.sub(pattern, replacer, value)
# 如果结果是纯数字字符串,转换回数字类型
try:
# 尝试转换为整数
if '.' not in result:
return int(result)
# 尝试转换为浮点数
else:
return float(result)
except ValueError:
# 不是数字,返回字符串
return result
def resolve_element(self, elem, vars_values):
"""
递归解析元素中的所有变量
Args:
elem: 元素dict, list, 或其他类型)
vars_values: 用户提供的变量值字典
Returns:
解析后的元素
"""
if isinstance(elem, dict):
return {k: self.resolve_element(v, vars_values)
for k, v in elem.items() if k != 'visible'}
elif isinstance(elem, list):
return [self.resolve_element(item, vars_values)
for item in elem]
elif isinstance(elem, str):
return self.resolve_value(elem, vars_values)
else:
return elem
def evaluate_condition(self, condition, vars_values):
"""
评估条件表达式(简单的存在性检查)
Args:
condition: 条件字符串,如 "{subtitle != ''}"
vars_values: 变量值字典
Returns:
bool: 条件是否满足
"""
# 简单实现:检查变量是否非空
# 匹配 {var_name != ''} 或 {var_name != ""}
pattern = r'\{(\w+)\s*!=\s*[\'\"]{2}\}'
match = re.match(pattern, condition)
if match:
var_name = match.group(1)
value = vars_values.get(var_name, '')
return value != ''
# 默认返回 True
return True
def render(self, vars_values):
"""
渲染模板,返回实际的元素列表
Args:
vars_values: 用户提供的变量值字典
Returns:
list: 渲染后的元素列表
Raises:
YAMLError: 缺少必需变量
"""
# 填充所有变量的默认值(如果用户未提供)
for var_name, var_def in self.vars_def.items():
if var_name not in vars_values:
# 检查是否是必需变量
if var_def.get('required', False):
# 必需变量必须有默认值或用户提供
if 'default' in var_def:
vars_values[var_name] = self.resolve_value(
var_def['default'], vars_values
)
else:
raise YAMLError(f"缺少必需变量: {var_name}")
else:
# 可选变量使用默认值(如果有)
if 'default' in var_def:
vars_values[var_name] = self.resolve_value(
var_def['default'], vars_values
)
# 渲染所有元素
rendered_elements = []
for elem in self.elements:
# 检查条件渲染
if 'visible' in elem:
if not self.evaluate_condition(elem['visible'], vars_values):
continue
# 深度解析元素中的所有变量引用
rendered_elem = self.resolve_element(elem, vars_values)
rendered_elements.append(rendered_elem)
return rendered_elements