| """ |
| Search/Replace utilities for applying targeted code changes. |
| Search/Replace utilities for applying targeted code changes. |
| """ |
|
|
| |
| 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 (SEARCH_START not in changes_text) and (DIVIDER not in changes_text) and (REPLACE_END not in changes_text): |
| try: |
| import re |
| updated_content = original_content |
| replaced_any_rule = False |
| |
| |
| 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 |
| |
| |
| 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: |
| return updated_content |
| except Exception: |
| |
| pass |
|
|
| |
| 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 |
| |
| |
| 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) |
| |
| |
| 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: |
| |
| 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 = {} |
| |
| |
| file_pattern = re.compile(r"^===\s+([^\n=]+?)\s+===\s*$", re.MULTILINE) |
| |
| |
| matches = list(file_pattern.finditer(changes_text)) |
| |
| if not matches: |
| |
| return {"__all__": changes_text} |
| |
| for i, match in enumerate(matches): |
| filename = match.group(1).strip() |
| start_pos = match.end() |
| |
| |
| 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 |
|
|