diff --git a/i18n_guardian/diff/diff_generator.py b/i18n_guardian/diff/diff_generator.py new file mode 100644 index 0000000..8d91851 --- /dev/null +++ b/i18n_guardian/diff/diff_generator.py @@ -0,0 +1,168 @@ +"""Diff generation for i18n fixes.""" + +import difflib +from dataclasses import dataclass, field +from pathlib import Path +from typing import List, Optional + +from rich.console import Console +from rich.syntax import Syntax + + +@dataclass +class DiffChange: + """Represents a single change in a diff.""" + + file_path: Path + original_line: int + original_text: str + new_text: str + new_line: Optional[int] = None + + +@dataclass +class DiffResult: + """Result of a diff operation.""" + + changes: List[DiffChange] = field(default_factory=list) + files_modified: int = 0 + total_changes: int = 0 + + +class DiffGenerator: + """Generate diffs for i18n fixes.""" + + def __init__(self, color: bool = True) -> None: + self.color = color + self.console = Console() + + def generate_diff( + self, + file_path: Path, + old_content: str, + new_content: str, + ) -> str: + """Generate a unified diff between old and new content.""" + diff = difflib.unified_diff( + old_content.splitlines(keepends=True), + new_content.splitlines(keepends=True), + fromfile=str(file_path), + tofile=str(file_path), + n=3, + ) + return "".join(diff) + + def preview_diff(self, diff: str) -> None: + """Preview a diff using Rich.""" + if self.color: + self.console.print(Syntax(diff, "diff", line_numbers=True)) + else: + self.console.print(diff) + + def apply_diff( + self, + file_path: Path, + diff: str, + backup: bool = True, + ) -> bool: + """Apply a diff to a file.""" + try: + if backup: + backup_path = file_path.with_suffix(".bak") + backup_path.write_text(file_path.read_text(encoding="utf-8"), encoding="utf-8") + + lines = file_path.read_text(encoding="utf-8").splitlines(keepends=True) + diff_lines = diff.splitlines(keepends=True) + + patched_lines = self._apply_diff_lines(lines, diff_lines) + + new_content = "".join(patched_lines) + file_path.write_text(new_content, encoding="utf-8") + + return True + except (OSError, UnicodeDecodeError) as e: + print(f"Error applying diff: {e}") + return False + + def _apply_diff_lines( + self, + lines: List[str], + diff_lines: List[str], + ) -> List[str]: + """Apply diff lines to document.""" + result = [] + + i = 0 + j = 0 + + while j < len(diff_lines): + diff_line = diff_lines[j] + + if diff_line.startswith("---") or diff_line.startswith("+++"): + j += 1 + continue + + if diff_line.startswith("@@"): + j += 1 + continue + + if diff_line.startswith("-"): + original_line = diff_line[1:] + added_content = "" + if "+" in original_line: + parts = original_line.split("+", 1) + original_line = parts[0] + added_content = parts[1] if len(parts) > 1 else "" + while i < len(lines) and lines[i].rstrip("\n") != original_line.rstrip("\n"): + result.append(lines[i]) + i += 1 + if i < len(lines): + i += 1 + if added_content: + result.append(added_content) + j += 1 + continue + + if diff_line.startswith("+"): + result.append(diff_line[1:]) + j += 1 + continue + + if diff_line.startswith(" "): + result.append(lines[i] if i < len(lines) else diff_line[1:]) + i += 1 + j += 1 + continue + + j += 1 + + while i < len(lines): + result.append(lines[i]) + i += 1 + + return result + + def create_replacement( + self, + original_text: str, + replacement: str, + i18n_function: str = "t", + ) -> str: + """Create a replacement string with i18n function call.""" + escaped = original_text.replace("\\", "\\\\").replace('"', '\\"') + return f'{i18n_function}("{escaped}")' + + def preview_change( + self, + file_path: Path, + original_text: str, + replacement: str, + line_number: int, + ) -> None: + """Preview a single change.""" + diff = self.generate_diff( + file_path, + original_text, + original_text.replace(original_text, replacement, 1), + ) + self.preview_diff(diff)