""" HTML renderer for report generation. """ from .rendererBaseTemplate import BaseRenderer from typing import Dict, Any, Tuple, List class RendererHtml(BaseRenderer): """Renders content to HTML format with format-specific extraction.""" @classmethod def get_supported_formats(cls) -> List[str]: """Return supported HTML formats.""" return ['html', 'htm'] @classmethod def get_format_aliases(cls) -> List[str]: """Return format aliases.""" return ['web', 'webpage'] @classmethod def get_priority(cls) -> int: """Return priority for HTML renderer.""" return 100 async def render(self, extracted_content: Dict[str, Any], title: str, user_prompt: str = None, ai_service=None) -> Tuple[str, str]: """Render extracted JSON content to HTML format using AI-analyzed styling.""" try: # Generate HTML using AI-analyzed styling html_content = await self._generate_html_from_json(extracted_content, title, user_prompt, ai_service) return html_content, "text/html" except Exception as e: self.logger.error(f"Error rendering HTML: {str(e)}") # Return minimal HTML fallback return f"{title}

{title}

Error rendering report: {str(e)}

", "text/html" async def _generate_html_from_json(self, json_content: Dict[str, Any], title: str, user_prompt: str = None, ai_service=None) -> str: """Generate HTML content from structured JSON document using AI-generated styling.""" try: # Get AI-generated styling definitions styles = await self._get_html_styles(user_prompt, ai_service) # Validate JSON structure if not isinstance(json_content, dict): raise ValueError("JSON content must be a dictionary") if "sections" not in json_content: raise ValueError("JSON content must contain 'sections' field") # Use title from JSON metadata if available, otherwise use provided title document_title = json_content.get("metadata", {}).get("title", title) # Build HTML document html_parts = [] # HTML document structure html_parts.append('') html_parts.append('') html_parts.append('') html_parts.append('') html_parts.append('') html_parts.append(f'{document_title}') html_parts.append('') html_parts.append('') html_parts.append('') # Document header html_parts.append(f'

{document_title}

') # Main content html_parts.append('
') # Process each section sections = json_content.get("sections", []) for section in sections: section_html = self._render_json_section(section, styles) if section_html: html_parts.append(section_html) html_parts.append('
') # Footer html_parts.append('') html_parts.append('') html_parts.append('') return '\n'.join(html_parts) except Exception as e: self.logger.error(f"Error generating HTML from JSON: {str(e)}") raise Exception(f"HTML generation failed: {str(e)}") async def _get_html_styles(self, user_prompt: str, ai_service=None) -> Dict[str, Any]: """Get HTML styling definitions using base template AI styling.""" style_schema = { "title": {"font_size": "2.5em", "color": "#1F4E79", "font_weight": "bold", "text_align": "center", "margin": "0 0 1em 0"}, "heading1": {"font_size": "2em", "color": "#2F2F2F", "font_weight": "bold", "text_align": "left", "margin": "1.5em 0 0.5em 0"}, "heading2": {"font_size": "1.5em", "color": "#4F4F4F", "font_weight": "bold", "text_align": "left", "margin": "1em 0 0.5em 0"}, "paragraph": {"font_size": "1em", "color": "#2F2F2F", "font_weight": "normal", "text_align": "left", "margin": "0 0 1em 0", "line_height": "1.6"}, "table": {"border": "1px solid #ddd", "border_collapse": "collapse", "width": "100%", "margin": "1em 0"}, "table_header": {"background": "#4F4F4F", "color": "#FFFFFF", "font_weight": "bold", "text_align": "center", "padding": "12px"}, "table_cell": {"background": "#FFFFFF", "color": "#2F2F2F", "font_weight": "normal", "text_align": "left", "padding": "8px", "border": "1px solid #ddd"}, "bullet_list": {"font_size": "1em", "color": "#2F2F2F", "margin": "0 0 1em 0", "padding_left": "20px"}, "code_block": {"font_family": "Courier New, monospace", "font_size": "0.9em", "color": "#2F2F2F", "background": "#F5F5F5", "padding": "1em", "border": "1px solid #ddd", "border_radius": "4px", "margin": "1em 0"}, "image": {"max_width": "100%", "height": "auto", "margin": "1em 0", "border_radius": "4px"}, "body": {"font_family": "Arial, sans-serif", "background": "#FFFFFF", "color": "#2F2F2F", "margin": "0", "padding": "20px"} } style_template = self._create_ai_style_template("html", user_prompt, style_schema) styles = await self._get_ai_styles(ai_service, style_template, self._get_default_html_styles()) # Validate and fix contrast issues return self._validate_html_styles_contrast(styles) def _validate_html_styles_contrast(self, styles: Dict[str, Any]) -> Dict[str, Any]: """Validate and fix contrast issues in AI-generated styles.""" try: # Fix table header contrast if "table_header" in styles: header = styles["table_header"] bg_color = header.get("background", "#FFFFFF") text_color = header.get("color", "#000000") # If both are white or both are dark, fix it if bg_color.upper() == "#FFFFFF" and text_color.upper() == "#FFFFFF": header["background"] = "#4F4F4F" header["color"] = "#FFFFFF" elif bg_color.upper() == "#000000" and text_color.upper() == "#000000": header["background"] = "#4F4F4F" header["color"] = "#FFFFFF" # Fix table cell contrast if "table_cell" in styles: cell = styles["table_cell"] bg_color = cell.get("background", "#FFFFFF") text_color = cell.get("color", "#000000") # If both are white or both are dark, fix it if bg_color.upper() == "#FFFFFF" and text_color.upper() == "#FFFFFF": cell["background"] = "#FFFFFF" cell["color"] = "#2F2F2F" elif bg_color.upper() == "#000000" and text_color.upper() == "#000000": cell["background"] = "#FFFFFF" cell["color"] = "#2F2F2F" return styles except Exception as e: self.logger.warning(f"Style validation failed: {str(e)}") return self._get_default_html_styles() def _get_default_html_styles(self) -> Dict[str, Any]: """Default HTML styles.""" return { "title": {"font_size": "2.5em", "color": "#1F4E79", "font_weight": "bold", "text_align": "center", "margin": "0 0 1em 0"}, "heading1": {"font_size": "2em", "color": "#2F2F2F", "font_weight": "bold", "text_align": "left", "margin": "1.5em 0 0.5em 0"}, "heading2": {"font_size": "1.5em", "color": "#4F4F4F", "font_weight": "bold", "text_align": "left", "margin": "1em 0 0.5em 0"}, "paragraph": {"font_size": "1em", "color": "#2F2F2F", "font_weight": "normal", "text_align": "left", "margin": "0 0 1em 0", "line_height": "1.6"}, "table": {"border": "1px solid #ddd", "border_collapse": "collapse", "width": "100%", "margin": "1em 0"}, "table_header": {"background": "#4F4F4F", "color": "#FFFFFF", "font_weight": "bold", "text_align": "center", "padding": "12px"}, "table_cell": {"background": "#FFFFFF", "color": "#2F2F2F", "font_weight": "normal", "text_align": "left", "padding": "8px", "border": "1px solid #ddd"}, "bullet_list": {"font_size": "1em", "color": "#2F2F2F", "margin": "0 0 1em 0", "padding_left": "20px"}, "code_block": {"font_family": "Courier New, monospace", "font_size": "0.9em", "color": "#2F2F2F", "background": "#F5F5F5", "padding": "1em", "border": "1px solid #ddd", "border_radius": "4px", "margin": "1em 0"}, "image": {"max_width": "100%", "height": "auto", "margin": "1em 0", "border_radius": "4px"}, "body": {"font_family": "Arial, sans-serif", "background": "#FFFFFF", "color": "#2F2F2F", "margin": "0", "padding": "20px"} } def _generate_css_styles(self, styles: Dict[str, Any]) -> str: """Generate CSS from style definitions.""" css_parts = [] # Body styles body_style = styles.get("body", {}) css_parts.append("body {") for property_name, value in body_style.items(): css_property = property_name.replace("_", "-") css_parts.append(f" {css_property}: {value};") css_parts.append("}") # Document title title_style = styles.get("title", {}) css_parts.append(".document-title {") for property_name, value in title_style.items(): css_property = property_name.replace("_", "-") css_parts.append(f" {css_property}: {value};") css_parts.append("}") # Headings for heading_level in ["heading1", "heading2"]: heading_style = styles.get(heading_level, {}) css_class = f"h{heading_level[-1]}" css_parts.append(f"{css_class} {{") for property_name, value in heading_style.items(): css_property = property_name.replace("_", "-") css_parts.append(f" {css_property}: {value};") css_parts.append("}") # Paragraphs paragraph_style = styles.get("paragraph", {}) css_parts.append("p {") for property_name, value in paragraph_style.items(): css_property = property_name.replace("_", "-") css_parts.append(f" {css_property}: {value};") css_parts.append("}") # Tables table_style = styles.get("table", {}) css_parts.append("table {") for property_name, value in table_style.items(): css_property = property_name.replace("_", "-") css_parts.append(f" {css_property}: {value};") css_parts.append("}") # Table headers table_header_style = styles.get("table_header", {}) css_parts.append("th {") for property_name, value in table_header_style.items(): css_property = property_name.replace("_", "-") css_parts.append(f" {css_property}: {value};") css_parts.append("}") # Table cells table_cell_style = styles.get("table_cell", {}) css_parts.append("td {") for property_name, value in table_cell_style.items(): css_property = property_name.replace("_", "-") css_parts.append(f" {css_property}: {value};") css_parts.append("}") # Lists bullet_list_style = styles.get("bullet_list", {}) css_parts.append("ul {") for property_name, value in bullet_list_style.items(): css_property = property_name.replace("_", "-") css_parts.append(f" {css_property}: {value};") css_parts.append("}") # Code blocks code_block_style = styles.get("code_block", {}) css_parts.append("pre {") for property_name, value in code_block_style.items(): css_property = property_name.replace("_", "-") css_parts.append(f" {css_property}: {value};") css_parts.append("}") # Images image_style = styles.get("image", {}) css_parts.append("img {") for property_name, value in image_style.items(): css_property = property_name.replace("_", "-") css_parts.append(f" {css_property}: {value};") css_parts.append("}") # Generated info css_parts.append(".generated-info {") css_parts.append(" font-size: 0.9em;") css_parts.append(" color: #666;") css_parts.append(" text-align: center;") css_parts.append(" margin-top: 2em;") css_parts.append(" padding-top: 1em;") css_parts.append(" border-top: 1px solid #ddd;") css_parts.append("}") return '\n'.join(css_parts) def _render_json_section(self, section: Dict[str, Any], styles: Dict[str, Any]) -> str: """Render a single JSON section to HTML using AI-generated styles.""" try: section_type = self._get_section_type(section) section_data = self._get_section_data(section) if section_type == "table": # Process the section data to extract table structure processed_data = self._process_section_by_type(section) return self._render_json_table(processed_data, styles) elif section_type == "bullet_list": # Process the section data to extract bullet list structure processed_data = self._process_section_by_type(section) return self._render_json_bullet_list(processed_data, styles) elif section_type == "heading": return self._render_json_heading(section_data, styles) elif section_type == "paragraph": return self._render_json_paragraph(section_data, styles) elif section_type == "code_block": # Process the section data to extract code block structure processed_data = self._process_section_by_type(section) return self._render_json_code_block(processed_data, styles) elif section_type == "image": # Process the section data to extract image structure processed_data = self._process_section_by_type(section) return self._render_json_image(processed_data, styles) else: # Fallback to paragraph for unknown types return self._render_json_paragraph(section_data, styles) except Exception as e: self.logger.warning(f"Error rendering section {self._get_section_id(section)}: {str(e)}") return f'
[Error rendering section: {str(e)}]
' def _render_json_table(self, table_data: Dict[str, Any], styles: Dict[str, Any]) -> str: """Render a JSON table to HTML using AI-generated styles.""" try: headers = table_data.get("headers", []) rows = table_data.get("rows", []) if not headers or not rows: return "" html_parts = [''] # Table header html_parts.append('') for header in headers: html_parts.append(f'') html_parts.append('') # Table body html_parts.append('') for row in rows: html_parts.append('') for cell_data in row: html_parts.append(f'') html_parts.append('') html_parts.append('') html_parts.append('
{header}
{cell_data}
') return '\n'.join(html_parts) except Exception as e: self.logger.warning(f"Error rendering table: {str(e)}") return "" def _render_json_bullet_list(self, list_data: Dict[str, Any], styles: Dict[str, Any]) -> str: """Render a JSON bullet list to HTML using AI-generated styles.""" try: items = list_data.get("items", []) if not items: return "" html_parts = ['') return '\n'.join(html_parts) except Exception as e: self.logger.warning(f"Error rendering bullet list: {str(e)}") return "" def _render_json_heading(self, heading_data: Dict[str, Any], styles: Dict[str, Any]) -> str: """Render a JSON heading to HTML using AI-generated styles.""" try: # Normalize non-dict inputs if isinstance(heading_data, str): heading_data = {"text": heading_data, "level": 2} elif isinstance(heading_data, list): # Render a list as bullet list under a default heading label return self._render_json_bullet_list({"items": heading_data}, styles) elif not isinstance(heading_data, dict): return "" level = heading_data.get("level", 1) text = heading_data.get("text", "") if text: level = max(1, min(6, level)) return f'{text}' return "" except Exception as e: self.logger.warning(f"Error rendering heading: {str(e)}") return "" def _render_json_paragraph(self, paragraph_data: Dict[str, Any], styles: Dict[str, Any]) -> str: """Render a JSON paragraph to HTML using AI-generated styles.""" try: # Normalize non-dict inputs if isinstance(paragraph_data, str): paragraph_data = {"text": paragraph_data} elif isinstance(paragraph_data, list): # Treat list as bullet list paragraph return self._render_json_bullet_list({"items": paragraph_data}, styles) elif not isinstance(paragraph_data, dict): return "" text = paragraph_data.get("text", "") if text: return f'

{text}

' return "" except Exception as e: self.logger.warning(f"Error rendering paragraph: {str(e)}") return "" def _render_json_code_block(self, code_data: Dict[str, Any], styles: Dict[str, Any]) -> str: """Render a JSON code block to HTML using AI-generated styles.""" try: code = code_data.get("code", "") language = code_data.get("language", "") if code: if language: return f'
{code}
' else: return f'
{code}
' return "" except Exception as e: self.logger.warning(f"Error rendering code block: {str(e)}") return "" def _render_json_image(self, image_data: Dict[str, Any], styles: Dict[str, Any]) -> str: """Render a JSON image to HTML.""" try: base64_data = image_data.get("base64Data", "") alt_text = image_data.get("altText", "Image") if base64_data: return f'{alt_text}' return "" except Exception as e: self.logger.warning(f"Error rendering image: {str(e)}") return f'
[Image: {image_data.get("altText", "Image")}]
'