Spaces:
Sleeping
Sleeping
| """ | |
| HTML generation and file management for dev flow. | |
| """ | |
| import os | |
| from datetime import datetime | |
| from pathlib import Path | |
| from logger import logger | |
| from config import HTML_OUTPUT_DIR | |
| class HTMLHandler: | |
| """Handles HTML generation and file management.""" | |
| def __init__(self, output_dir=HTML_OUTPUT_DIR): | |
| """Initialize HTML handler.""" | |
| self.output_dir = output_dir | |
| Path(self.output_dir).mkdir(exist_ok=True) | |
| logger.debug(f"HTMLHandler initialized with output dir: {output_dir}") | |
| def generate_html(self, title, body, tags, image_url=None, original_url=None): | |
| """ | |
| Generate HTML content from article data. | |
| Args: | |
| title (str): Article title | |
| body (str): Article body (HTML) | |
| tags (list): List of tags | |
| image_url (str, optional): Featured image URL | |
| original_url (str, optional): Original article URL | |
| Returns: | |
| str: Generated HTML content | |
| """ | |
| try: | |
| # Format tags as HTML | |
| tags_html = " ".join([f'<span class="tag">{tag}</span>' for tag in tags]) | |
| # Build featured image section | |
| image_section = "" | |
| if image_url: | |
| image_section = ( | |
| f'<div class="featured-image">' | |
| f'<img src="{image_url}" alt="{title}">' | |
| f"</div>" | |
| ) | |
| # Generate complete HTML | |
| html_content = f"""<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>{title}</title> | |
| <style> | |
| * {{ | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| }} | |
| body {{ | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; | |
| line-height: 1.6; | |
| color: #333; | |
| background-color: #f5f5f5; | |
| }} | |
| .container {{ | |
| max-width: 800px; | |
| margin: 0 auto; | |
| padding: 40px 20px; | |
| background-color: white; | |
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | |
| }} | |
| header {{ | |
| margin-bottom: 30px; | |
| border-bottom: 3px solid #007bff; | |
| padding-bottom: 20px; | |
| }} | |
| h1 {{ | |
| font-size: 2.2em; | |
| margin-bottom: 15px; | |
| line-height: 1.3; | |
| color: #1a1a1a; | |
| }} | |
| .meta {{ | |
| font-size: 0.9em; | |
| color: #666; | |
| margin-bottom: 10px; | |
| }} | |
| .tags {{ | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| margin-top: 10px; | |
| }} | |
| .tag {{ | |
| background-color: #e9ecef; | |
| color: #007bff; | |
| padding: 4px 12px; | |
| border-radius: 20px; | |
| font-size: 0.85em; | |
| font-weight: 500; | |
| }} | |
| .featured-image {{ | |
| margin: 30px 0; | |
| text-align: center; | |
| }} | |
| .featured-image img {{ | |
| max-width: 100%; | |
| height: auto; | |
| border-radius: 8px; | |
| box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); | |
| }} | |
| .content {{ | |
| font-size: 1.1em; | |
| margin-bottom: 30px; | |
| line-height: 1.8; | |
| }} | |
| .content p {{ | |
| margin-bottom: 15px; | |
| text-align: justify; | |
| }} | |
| .content figure {{ | |
| margin: 20px 0; | |
| }} | |
| .content img {{ | |
| max-width: 100%; | |
| height: auto; | |
| border-radius: 6px; | |
| }} | |
| footer {{ | |
| margin-top: 30px; | |
| padding-top: 20px; | |
| border-top: 1px solid #ddd; | |
| font-size: 0.9em; | |
| color: #666; | |
| text-align: center; | |
| }} | |
| /* Desktop: max-width 1200px and above */ | |
| @media (min-width: 1200px) {{ | |
| .featured-image img {{ | |
| max-width: 300px; | |
| }} | |
| }} | |
| /* Tablet: 768px to 1199px */ | |
| @media (min-width: 768px) and (max-width: 1199px) {{ | |
| .featured-image img {{ | |
| max-width: 250px; | |
| }} | |
| }} | |
| /* Responsive container and content font size remain */ | |
| @media (max-width: 600px) {{ | |
| .container {{ | |
| padding: 20px; | |
| }} | |
| h1 {{ | |
| font-size: 1.6em; | |
| }} | |
| .content {{ | |
| font-size: 1em; | |
| }} | |
| }} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <h1>{title}</h1> | |
| <div class="meta"> | |
| Generated on {datetime.now().strftime('%B %d, %Y at %I:%M %p')} | |
| </div> | |
| <div class="tags"> | |
| {tags_html} | |
| </div> | |
| </header> | |
| {image_section} | |
| <div class="content"> | |
| {body} | |
| </div> | |
| </div> | |
| </body> | |
| </html>""" | |
| logger.debug("HTML content generated successfully") | |
| return html_content | |
| except Exception as e: | |
| logger.error(f"Failed to generate HTML: {e}") | |
| raise | |
| def save_html(self, html_content, filename=None): | |
| """ | |
| Save HTML content to file. | |
| Args: | |
| html_content (str): HTML content to save | |
| filename (str, optional): Filename to use. If None, generates timestamp-based name. | |
| Returns: | |
| str: Full path to saved file | |
| """ | |
| try: | |
| if not filename: | |
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
| filename = f"article_{timestamp}.html" | |
| file_path = os.path.join(self.output_dir, filename) | |
| with open(file_path, "w", encoding="utf-8") as f: | |
| f.write(html_content) | |
| logger.info(f"✓ HTML file saved: {file_path}") | |
| return file_path | |
| except Exception as e: | |
| logger.error(f"Failed to save HTML file: {e}") | |
| raise | |
| def generate_and_save(self, title, body, tags, image_url=None, original_url=None, filename=None): | |
| """ | |
| Generate and save HTML in one call. | |
| Args: | |
| title (str): Article title | |
| body (str): Article body (HTML) | |
| tags (list): List of tags | |
| image_url (str, optional): Featured image URL | |
| original_url (str, optional): Original article URL | |
| filename (str, optional): Custom filename | |
| Returns: | |
| str: Full path to saved file | |
| """ | |
| html_content = self.generate_html(title, body, tags, image_url, original_url) | |
| return self.save_html(html_content, filename) | |