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:
172
renderers/html_renderer.py
Normal file
172
renderers/html_renderer.py
Normal file
@@ -0,0 +1,172 @@
|
||||
"""
|
||||
HTML 渲染器模块
|
||||
|
||||
将元素对象渲染为 HTML 代码,用于浏览器预览。
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from core.elements import TextElement, ImageElement, ShapeElement, TableElement
|
||||
|
||||
|
||||
# 固定 DPI 用于单位转换
|
||||
DPI = 96
|
||||
|
||||
|
||||
class HtmlRenderer:
|
||||
"""HTML 渲染器,将元素渲染为 HTML"""
|
||||
|
||||
def render_slide(self, slide_data, index, base_path):
|
||||
"""
|
||||
渲染单个幻灯片为 HTML
|
||||
|
||||
Args:
|
||||
slide_data: 包含 background 和 elements 的字典
|
||||
index: 幻灯片索引
|
||||
base_path: 基础路径
|
||||
|
||||
Returns:
|
||||
str: 幻灯片的 HTML 代码
|
||||
"""
|
||||
elements_html = ""
|
||||
|
||||
bg_style = ""
|
||||
if slide_data.get('background'):
|
||||
bg = slide_data['background']
|
||||
if 'color' in bg:
|
||||
bg_style = f"background: {bg['color']};"
|
||||
|
||||
for elem in slide_data.get('elements', []):
|
||||
try:
|
||||
if isinstance(elem, TextElement):
|
||||
elements_html += self.render_text(elem)
|
||||
elif isinstance(elem, ShapeElement):
|
||||
elements_html += self.render_shape(elem)
|
||||
elif isinstance(elem, TableElement):
|
||||
elements_html += self.render_table(elem)
|
||||
elif isinstance(elem, ImageElement):
|
||||
elements_html += self.render_image(elem, base_path)
|
||||
except Exception as e:
|
||||
elements_html += f'<div class="element" style="color: red;">渲染错误: {str(e)}</div>'
|
||||
|
||||
return f'''
|
||||
<div class="slide" style="{bg_style}">
|
||||
<div class="slide-number">幻灯片 {index + 1}</div>
|
||||
{elements_html}
|
||||
</div>
|
||||
'''
|
||||
|
||||
def render_text(self, elem: TextElement):
|
||||
"""
|
||||
将文本元素转换为 HTML
|
||||
|
||||
Args:
|
||||
elem: TextElement 对象
|
||||
|
||||
Returns:
|
||||
str: HTML 代码
|
||||
"""
|
||||
style = f"""
|
||||
left: {elem.box[0] * DPI}px;
|
||||
top: {elem.box[1] * DPI}px;
|
||||
width: {elem.box[2] * DPI}px;
|
||||
height: {elem.box[3] * DPI}px;
|
||||
font-size: {elem.font.get('size', 16)}pt;
|
||||
color: {elem.font.get('color', '#000000')};
|
||||
text-align: {elem.font.get('align', 'left')};
|
||||
{'font-weight: bold;' if elem.font.get('bold') else ''}
|
||||
{'font-style: italic;' if elem.font.get('italic') else ''}
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: normal;
|
||||
overflow-wrap: break-word;
|
||||
"""
|
||||
|
||||
content = elem.content.replace('<', '<').replace('>', '>')
|
||||
return f'<div class="element text-element" style="{style}">{content}</div>'
|
||||
|
||||
def render_shape(self, elem: ShapeElement):
|
||||
"""
|
||||
将形状元素转换为 HTML
|
||||
|
||||
Args:
|
||||
elem: ShapeElement 对象
|
||||
|
||||
Returns:
|
||||
str: HTML 代码
|
||||
"""
|
||||
border_radius = {
|
||||
'rectangle': '0',
|
||||
'ellipse': '50%',
|
||||
'rounded_rectangle': '8px'
|
||||
}.get(elem.shape, '0')
|
||||
|
||||
style = f"""
|
||||
left: {elem.box[0] * DPI}px;
|
||||
top: {elem.box[1] * DPI}px;
|
||||
width: {elem.box[2] * DPI}px;
|
||||
height: {elem.box[3] * DPI}px;
|
||||
background: {elem.fill if elem.fill else 'transparent'};
|
||||
border-radius: {border_radius};
|
||||
"""
|
||||
|
||||
if elem.line:
|
||||
style += f"""
|
||||
border: {elem.line.get('width', 1)}pt solid {elem.line.get('color', '#000000')};
|
||||
"""
|
||||
|
||||
return f'<div class="element shape-element" style="{style}"></div>'
|
||||
|
||||
def render_table(self, elem: TableElement):
|
||||
"""
|
||||
将表格元素转换为 HTML
|
||||
|
||||
Args:
|
||||
elem: TableElement 对象
|
||||
|
||||
Returns:
|
||||
str: HTML 代码
|
||||
"""
|
||||
table_style = f"""
|
||||
left: {elem.position[0] * DPI}px;
|
||||
top: {elem.position[1] * DPI}px;
|
||||
"""
|
||||
|
||||
rows_html = ""
|
||||
for i, row in enumerate(elem.data):
|
||||
cells_html = ""
|
||||
for cell in row:
|
||||
cell_style = f"font-size: {elem.style.get('font_size', 14)}pt;"
|
||||
|
||||
if i == 0:
|
||||
if 'header_bg' in elem.style:
|
||||
cell_style += f"background: {elem.style['header_bg']};"
|
||||
if 'header_color' in elem.style:
|
||||
cell_style += f"color: {elem.style['header_color']};"
|
||||
|
||||
cell_content = str(cell).replace('<', '<').replace('>', '>')
|
||||
cells_html += f'<td style="{cell_style}">{cell_content}</td>'
|
||||
rows_html += f'<tr>{cells_html}</tr>'
|
||||
|
||||
return f'<table class="element table-element" style="{table_style}">{rows_html}</table>'
|
||||
|
||||
def render_image(self, elem: ImageElement, base_path):
|
||||
"""
|
||||
将图片元素转换为 HTML
|
||||
|
||||
Args:
|
||||
elem: ImageElement 对象
|
||||
base_path: 基础路径
|
||||
|
||||
Returns:
|
||||
str: HTML 代码
|
||||
"""
|
||||
img_path = Path(base_path) / elem.src if base_path else Path(elem.src)
|
||||
|
||||
style = f"""
|
||||
left: {elem.box[0] * DPI}px;
|
||||
top: {elem.box[1] * DPI}px;
|
||||
width: {elem.box[2] * DPI}px;
|
||||
height: {elem.box[3] * DPI}px;
|
||||
"""
|
||||
|
||||
return f'<img class="element image-element" src="file://{img_path.absolute()}" style="{style}">'
|
||||
Reference in New Issue
Block a user