""" HTML formatter for generating GitHub-style diff output. Generates complete HTML documents with embedded CSS for both unified and side-by-side diff views. """ import html from typing import Dict, Optional, Union from .diff_engine import DiffResult, DiffLine # Theme color constants (dark theme matching ComfyUI interface) COLOR_BG_DARK = "#1e1e1e" COLOR_BG_HEADER = "#252526" COLOR_BORDER = "#3c3c3c" COLOR_TEXT = "#d4d4d4" COLOR_TEXT_MUTED = "#9d9d9d" COLOR_LINE_NUM = "#6e7681" COLOR_ADDED = "#4ec9b0" COLOR_DELETED = "#f14c4c" COLOR_ADDED_BG = "#1e3a1e" COLOR_DELETED_BG = "#3a1e1e" COLOR_CHAR_ADDED_BG = "#2d5a2d" COLOR_CHAR_DELETED_BG = "#5a2d2d" # Dark theme CSS for diff display DIFF_CSS = """ """ def format_diff_html(diff_result: DiffResult, view_mode: str = "unified") -> str: """ Generate complete HTML for diff display. Args: diff_result: The computed diff result view_mode: 'unified' or 'side_by_side' Returns: Complete HTML document string """ if view_mode == "side_by_side": return format_side_by_side_view(diff_result) return format_unified_view(diff_result) def format_unified_view(diff_result: DiffResult) -> str: """ Generate unified diff view (similar to git diff). Shows all lines in a single column with +/- prefixes. """ stats = diff_result.stats header = _generate_header(stats) # Check for no changes if stats["additions"] == 0 and stats["deletions"] == 0: return _generate_html_document( f""" {header}
✓
No differences found
""", "unified", ) rows = [] for line in diff_result.lines: row_class = _get_row_class(line.change_type) line_num_a = line.line_num_a if line.line_num_a is not None else "" line_num_b = line.line_num_b if line.line_num_b is not None else "" content = _format_content_with_char_diffs(line) prefix = _get_prefix(line.change_type) rows.append( f""" {line_num_a} {line_num_b} {prefix}{content} """ ) table = f""" {''.join(rows)}
""" return _generate_html_document(f"{header}{table}", "unified") def format_side_by_side_view(diff_result: DiffResult) -> str: """ Generate side-by-side diff view. Shows original and modified text in two columns. """ stats = diff_result.stats header = _generate_header(stats) # Check for no changes if stats["additions"] == 0 and stats["deletions"] == 0: return _generate_html_document( f""" {header}
✓
No differences found
""", "side-by-side", ) # Group lines for side-by-side display rows = [] i = 0 lines = diff_result.lines while i < len(lines): line = lines[i] if line.change_type == "unchanged": # Unchanged line - show on both sides content = _format_content_with_char_diffs(line) rows.append(_create_side_by_side_row( line.line_num_a, content, line.line_num_b, content, "line-unchanged" )) i += 1 elif line.change_type == "deleted": # Check if this is a paired line from a replace opcode AND next line is its pair if (line.is_paired and i + 1 < len(lines) and lines[i + 1].change_type == "added" and lines[i + 1].is_paired): # Paired deletion and addition from same replace opcode del_line = line add_line = lines[i + 1] left_content = _format_content_with_char_diffs(del_line, for_deleted=True) right_content = _format_content_with_char_diffs(add_line, for_added=True) rows.append(_create_side_by_side_row( del_line.line_num_a, left_content, add_line.line_num_b, right_content, "line-modified" )) i += 2 else: # Deletion only content = _format_content_with_char_diffs(line, for_deleted=True) rows.append(_create_side_by_side_row( line.line_num_a, content, "", "", "line-deleted" )) i += 1 elif line.change_type == "added": # Addition only (no paired deletion) content = _format_content_with_char_diffs(line, for_added=True) rows.append(_create_side_by_side_row( "", "", line.line_num_b, content, "line-added" )) i += 1 else: i += 1 table = f""" {''.join(rows)}
""" return _generate_html_document(f"{header}{table}", "side-by-side") def _create_side_by_side_row( left_num: Union[int, str], left_content: str, right_num: Union[int, str], right_content: str, row_class: str ) -> str: """ Create a side-by-side table row with left and right columns. Args: left_num: Line number for left side (or empty string) left_content: HTML content for left side right_num: Line number for right side (or empty string) right_content: HTML content for right side row_class: CSS class for the row (line-unchanged, line-modified, etc.) Returns: HTML string for the table row """ left_cell_class = "diff-content left" right_cell_class = "diff-content right" if not left_content and left_num == "": left_cell_class += " empty-cell" if not right_content and right_num == "": right_cell_class += " empty-cell" # Add background colors for modified rows (dark theme) left_bg = "" right_bg = "" if row_class == "line-deleted" or row_class == "line-modified": left_bg = ' style="background-color: #3a1e1e;"' if row_class == "line-added" or row_class == "line-modified": right_bg = ' style="background-color: #1e3a1e;"' return f""" {left_num} {left_content} {right_num} {right_content} """ def _generate_header(stats: Dict[str, int]) -> str: """ Generate the diff header with addition/deletion statistics. Args: stats: Dictionary with 'additions' and 'deletions' counts Returns: HTML string for the header section """ additions = stats.get("additions", 0) deletions = stats.get("deletions", 0) return f"""
+{additions}   -{deletions}
""" def _generate_html_document(body_content: str, view_class: str) -> str: """ Generate a complete HTML document with embedded CSS. Args: body_content: HTML content for the body view_class: CSS class for the view mode ('unified' or 'side-by-side') Returns: Complete HTML document string """ return f""" {DIFF_CSS}
{body_content}
""" def _get_row_class(change_type: str) -> str: """Get the CSS class for a row based on change type.""" return { "added": "line-added", "deleted": "line-deleted", "unchanged": "line-unchanged", }.get(change_type, "") def _get_prefix(change_type: str) -> str: """Get the prefix indicator for unified view.""" if change_type == "added": return '+' elif change_type == "deleted": return '-' return ' ' def _format_content_with_char_diffs( line: DiffLine, for_deleted: bool = False, for_added: bool = False ) -> str: """ Format line content with character-level highlighting. Args: line: The DiffLine to format for_deleted: If True, format for left side of side-by-side (show deletions only) for_added: If True, format for right side of side-by-side (show additions only) Returns: HTML string with highlighted character differences """ content = line.content.rstrip("\n\r") if not line.char_diffs: return html.escape(content) # Determine what to show based on context: # - Side-by-side mode: for_deleted or for_added will be True # - Unified mode: both are False, determine from line.change_type # Each line should only show its own content (deleted lines show deletions, added lines show additions) show_deletions = for_deleted or (not for_added and line.change_type == "deleted") show_additions = for_added or (not for_deleted and line.change_type == "added") parts = [] for change_type, text in line.char_diffs: escaped = html.escape(text.rstrip("\n\r")) if change_type == "equal": parts.append(escaped) elif change_type == "delete" and show_deletions: parts.append(f'{escaped}') elif change_type == "insert" and show_additions: parts.append(f'{escaped}') # Skip insert on deleted lines, skip delete on added lines return "".join(parts)