"""Jinja2テンプレートからHTML生成モジュール."""
import logging
from datetime import datetime
from pathlib import Path
from typing import Any, Optional
from jinja2 import Environment, FileSystemLoader, select_autoescape
logger = logging.getLogger(__name__)
class HTMLGenerator:
"""Jinja2テンプレートからHTML生成を行うクラス."""
def __init__(self):
"""初期化."""
template_dir = Path(__file__).parent / "templates"
template_dir.mkdir(exist_ok=True)
self.env = Environment(loader=FileSystemLoader(str(template_dir)), autoescape=select_autoescape(["html", "xml"]))
def generate(self, template_name: str, context: dict, output_path: str) -> str:
"""HTMLレポート生成.
Args:
template_name: テンプレートファイル名
context: テンプレートに渡すコンテキスト
output_path: 出力先HTMLファイルパス
Returns:
str: 生成されたHTMLファイルのパス
Raises:
FileNotFoundError: テンプレートが見つからない場合
"""
try:
template = self.env.get_template(template_name)
except Exception as e:
raise FileNotFoundError(f"テンプレートが見つかりません: {template_name}") from e
# 基本コンテキストをマージ
full_context = self._get_base_context()
full_context.update(context)
# HTMLレンダリング
logger.info(f"HTMLレンダリング開始: {template_name}")
html = template.render(**full_context)
# ファイル出力
output_file = Path(output_path)
output_file.parent.mkdir(parents=True, exist_ok=True)
with open(output_file, "w", encoding="utf-8") as f:
f.write(html)
logger.info(f"HTMLレポート生成完了: {output_file}")
return str(output_file)
def update_ai_analysis(self, report_path: str, issues: list[dict], solutions: list[dict]) -> str:
"""既存HTMLレポートにAI分析結果を追加.
Args:
report_path: 既存のHTMLレポートパス
issues: 課題リスト
solutions: 解決策リスト
Returns:
str: 更新されたHTMLファイルのパス
Raises:
FileNotFoundError: レポートファイルが見つからない場合
"""
report_file = Path(report_path)
if not report_file.exists():
raise FileNotFoundError(f"レポートファイルが見つかりません: {report_path}")
logger.info(f"AI分析結果の追加開始: {report_path}")
# 既存HTMLを読み込む
with open(report_file, "r", encoding="utf-8") as f:
html_content = f.read()
# issuesセクションのHTML生成
issues_html = self._generate_issues_html(issues)
# solutionsセクションのHTML生成
solutions_html = self._generate_solutions_html(solutions)
# HTMLを更新(ai_warningセクションを削除し、issues/solutionsを挿入)
import re
# 警告カードを削除
html_content = re.sub(
r'<!-- AI_WARNING_START -->.*?<!-- AI_WARNING_END -->',
'',
html_content,
flags=re.DOTALL
)
# issuesセクションを更新
html_content = re.sub(
r'<!-- AI_ISSUES_START -->.*?<!-- AI_ISSUES_END -->',
f'<!-- AI_ISSUES_START -->\n{issues_html} <!-- AI_ISSUES_END -->',
html_content,
flags=re.DOTALL
)
# solutionsセクション全体を更新
html_content = re.sub(
r'<!-- AI_SOLUTIONS_START -->.*?<!-- AI_SOLUTIONS_END -->',
f'<!-- AI_SOLUTIONS_START -->\n{solutions_html} <!-- AI_SOLUTIONS_END -->',
html_content,
flags=re.DOTALL
)
# エグゼクティブサマリーの「検出課題」数値を更新
html_content = re.sub(
r'(<span class="metric-value">)\d+(</span>\s*<span class="metric-label">検出課題</span>)',
f'\\g<1>{len(issues)}\\g<2>',
html_content
)
# 更新されたHTMLを保存
with open(report_file, "w", encoding="utf-8") as f:
f.write(html_content)
logger.info(f"AI分析結果の追加完了: {report_file}")
return str(report_file)
def _generate_issues_html(self, issues: list[dict]) -> str:
"""課題セクションのHTML生成.
Args:
issues: 課題リスト
Returns:
str: 課題セクションのHTML
"""
if not issues:
return ""
html = '<ul class="issue-list">\n'
for i, issue in enumerate(issues, 1):
priority = issue.get("priority", "中").lower()
title = issue.get("title", "")
detail = issue.get("detail", "")
impact = issue.get("impact", "")
html += f''' <li class="issue-item priority-{priority}">
<div class="issue-title">{i}. {title}</div>
<div class="issue-detail">{detail}</div>
<div class="issue-meta">
<span class="issue-badge">優先度: {issue.get("priority", "中")}</span>
'''
if impact:
html += f' <span class="issue-badge">影響範囲: {impact}</span>\n'
html += ' </div>\n'
html += ' </li>\n'
html += ' </ul>\n'
return html
def _generate_solutions_html(self, solutions: list[dict]) -> str:
"""解決策セクションのHTML生成.
Args:
solutions: 解決策リスト
Returns:
str: 解決策セクションのHTML
"""
if not solutions:
return ""
html = ''
for i, solution in enumerate(solutions, 1):
actions = solution.get("actions", [])
difficulty = solution.get("difficulty", "")
period = solution.get("period", "")
expected_effect = solution.get("expected_effect", "")
html += f''' <div class="solution-card">
<div class="solution-title">改善提案 {i}</div>
'''
if actions:
html += ' <div>\n'
html += ' <strong>アクションプラン:</strong>\n'
html += ' <ul class="action-list">\n'
for action in actions:
html += f' <li>{action}</li>\n'
html += ' </ul>\n'
html += ' </div>\n\n'
html += ' <div class="issue-meta">\n'
if difficulty:
html += f' <span class="issue-badge">難易度: {difficulty}</span>\n'
if period:
html += f' <span class="issue-badge">期間: {period}</span>\n'
html += ' </div>\n\n'
if expected_effect:
html += ' <div style="margin-top: 1rem; opacity: 0.9;">\n'
html += f' <strong>期待効果:</strong> {expected_effect}\n'
html += ' </div>\n'
html += ' </div>\n\n'
return html
def _get_base_context(self) -> dict[str, Any]:
"""基本コンテキスト取得.
Returns:
dict: 基本コンテキスト
"""
return {"generated_at": datetime.now().strftime("%Y年%m月%d日 %H:%M:%S"), "version": "0.1.0"}