""" Search/Replace utilities for applying targeted code changes. Extracted from anycoder_app/parsers.py for use in backend. """ # Search/Replace block markers SEARCH_START = "\u003c\u003c\u003c\u003c\u003c\u003c\u003c SEARCH" DIVIDER = "=======" REPLACE_END = "\u003e\u003e\u003e\u003e\u003e\u003e\u003e REPLACE" def apply_search_replace_changes(original_content: str, changes_text: str) -> str: """Apply search/replace changes to content (HTML, Python, JS, CSS, etc.) Args: original_content: The original file content to modify changes_text: Text containing SEARCH/REPLACE blocks Returns: Modified content with all search/replace blocks applied """ if not changes_text.strip(): return original_content # If the model didn't use the block markers, try a CSS-rule fallback where # provided blocks like `.selector { ... }` replace matching CSS rules. if (SEARCH_START not in changes_text) and (DIVIDER not in changes_text) and (REPLACE_END not in changes_text): try: import re # Local import to avoid global side effects updated_content = original_content replaced_any_rule = False # Find CSS-like rule blocks in the changes_text # This is a conservative matcher that looks for `selector { ... }` css_blocks = re.findall(r"([^{]+)\{([\s\S]*?)\}", changes_text, flags=re.MULTILINE) for selector_raw, body_raw in css_blocks: selector = selector_raw.strip() body = body_raw.strip() if not selector: continue # Build a regex to find the existing rule for this selector # Capture opening `{` and closing `}` to preserve them; replace inner body. pattern = re.compile(rf"({re.escape(selector)}\s*\{{)([\s\S]*?)(\}})") def _replace_rule(match): nonlocal replaced_any_rule replaced_any_rule = True prefix, existing_body, suffix = match.groups() # Preserve indentation of the existing first body line if present first_line_indent = "" for line in existing_body.splitlines(): stripped = line.lstrip(" \t") if stripped: first_line_indent = line[: len(line) - len(stripped)] break # Re-indent provided body with the detected indent if body: new_body_lines = [first_line_indent + line if line.strip() else line for line in body.splitlines()] new_body_text = "\n" + "\n".join(new_body_lines) + "\n" else: new_body_text = existing_body # If empty body provided, keep existing return f"{prefix}{new_body_text}{suffix}" updated_content, num_subs = pattern.subn(_replace_rule, updated_content, count=1) if replaced_any_rule: return updated_content except Exception: # Fallback silently to the standard block-based application pass # Split the changes text into individual search/replace blocks blocks = [] current_block = "" lines = changes_text.split('\n') for line in lines: if line.strip() == SEARCH_START: if current_block.strip(): blocks.append(current_block.strip()) current_block = line + '\n' elif line.strip() == REPLACE_END: current_block += line + '\n' blocks.append(current_block.strip()) current_block = "" else: current_block += line + '\n' if current_block.strip(): blocks.append(current_block.strip()) modified_content = original_content for block in blocks: if not block.strip(): continue # Parse the search/replace block lines = block.split('\n') search_lines = [] replace_lines = [] in_search = False in_replace = False for line in lines: if line.strip() == SEARCH_START: in_search = True in_replace = False elif line.strip() == DIVIDER: in_search = False in_replace = True elif line.strip() == REPLACE_END: in_replace = False elif in_search: search_lines.append(line) elif in_replace: replace_lines.append(line) # Apply the search/replace if search_lines: search_text = '\n'.join(search_lines).strip() replace_text = '\n'.join(replace_lines).strip() if search_text in modified_content: modified_content = modified_content.replace(search_text, replace_text) else: # If exact block match fails, attempt a CSS-rule fallback using the replace_text try: import re updated_content = modified_content replaced_any_rule = False css_blocks = re.findall(r"([^{]+)\{([\s\S]*?)\}", replace_text, flags=re.MULTILINE) for selector_raw, body_raw in css_blocks: selector = selector_raw.strip() body = body_raw.strip() if not selector: continue pattern = re.compile(rf"({re.escape(selector)}\s*\{{)([\s\S]*?)(\}})") def _replace_rule(match): nonlocal replaced_any_rule replaced_any_rule = True prefix, existing_body, suffix = match.groups() first_line_indent = "" for line in existing_body.splitlines(): stripped = line.lstrip(" \t") if stripped: first_line_indent = line[: len(line) - len(stripped)] break if body: new_body_lines = [first_line_indent + line if line.strip() else line for line in body.splitlines()] new_body_text = "\n" + "\n".join(new_body_lines) + "\n" else: new_body_text = existing_body return f"{prefix}{new_body_text}{suffix}" updated_content, num_subs = pattern.subn(_replace_rule, updated_content, count=1) if replaced_any_rule: modified_content = updated_content else: print(f"[Search/Replace] Warning: Search text not found in content: {search_text[:100]}...") except Exception: print(f"[Search/Replace] Warning: Search text not found in content: {search_text[:100]}...") return modified_content def has_search_replace_blocks(text: str) -> bool: """Check if text contains SEARCH/REPLACE block markers. Args: text: Text to check Returns: True if text contains search/replace markers, False otherwise """ return (SEARCH_START in text) and (DIVIDER in text) and (REPLACE_END in text) def parse_file_specific_changes(changes_text: str) -> dict: """Parse changes that specify which files to modify. Looks for patterns like: === components/Header.jsx === \u003c\u003c\u003c\u003c\u003c\u003c\u003c SEARCH ... Returns: Dict mapping filename -> search/replace changes for that file """ import re file_changes = {} # Pattern to match file sections: === filename === file_pattern = re.compile(r"^===\s+([^\n=]+?)\s+===\s*$", re.MULTILINE) # Find all file sections matches = list(file_pattern.finditer(changes_text)) if not matches: # No file-specific sections, treat entire text as changes return {"__all__": changes_text} for i, match in enumerate(matches): filename = match.group(1).strip() start_pos = match.end() # Find the end of this file's section (start of next file or end of text) if i + 1 < len(matches): end_pos = matches[i + 1].start() else: end_pos = len(changes_text) file_content = changes_text[start_pos:end_pos].strip() if file_content: file_changes[filename] = file_content return file_changes