|
|
""" |
|
|
Search/Replace utilities for applying targeted code changes. |
|
|
Extracted from anycoder_app/parsers.py for use in backend. |
|
|
""" |
|
|
|
|
|
|
|
|
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 |
|
|
|