main
py 177 lines 6.34 KB
Raw
1 import re
2 import sys
3 import time
4 from helpers import files
5
6 def sanitize_string(s: str, encoding: str = "utf-8") -> str:
7 # Replace surrogates and invalid unicode with replacement character
8 if not isinstance(s, str):
9 s = str(s)
10 return s.encode(encoding, 'replace').decode(encoding, 'replace')
11
12 def calculate_valid_match_lengths(first: bytes | str, second: bytes | str,
13 deviation_threshold: int = 5,
14 deviation_reset: int = 5,
15 ignore_patterns: list[bytes|str] = [],
16 debug: bool = False) -> tuple[int, int]:
17
18 first_length = len(first)
19 second_length = len(second)
20
21 i, j = 0, 0
22 deviations = 0
23 matched_since_deviation = 0
24 last_matched_i, last_matched_j = 0, 0 # Track the last matched index
25
26 def skip_ignored_patterns(s, index):
27 """Skip characters in `s` that match any pattern in `ignore_patterns` starting from `index`."""
28 while index < len(s):
29 for pattern in ignore_patterns:
30 match = re.match(pattern, s[index:])
31 if match:
32 index += len(match.group(0))
33 break
34 else:
35 break
36 return index
37
38 while i < first_length and j < second_length:
39 # Skip ignored patterns
40 i = skip_ignored_patterns(first, i)
41 j = skip_ignored_patterns(second, j)
42
43 if i < first_length and j < second_length and first[i] == second[j]:
44 last_matched_i, last_matched_j = i + 1, j + 1 # Update last matched position
45 i += 1
46 j += 1
47 matched_since_deviation += 1
48
49 # Reset the deviation counter if we've matched enough characters since the last deviation
50 if matched_since_deviation >= deviation_reset:
51 deviations = 0
52 matched_since_deviation = 0
53 else:
54 # Determine the look-ahead based on the remaining deviation threshold
55 look_ahead = deviation_threshold - deviations
56
57 # Look ahead to find the best match within the remaining deviation allowance
58 best_match = None
59 for k in range(1, look_ahead + 1):
60 if i + k < first_length and j < second_length and first[i + k] == second[j]:
61 best_match = ('i', k)
62 break
63 if j + k < second_length and i < first_length and first[i] == second[j + k]:
64 best_match = ('j', k)
65 break
66
67 if best_match:
68 if best_match[0] == 'i':
69 i += best_match[1]
70 elif best_match[0] == 'j':
71 j += best_match[1]
72 else:
73 i += 1
74 j += 1
75
76 deviations += 1
77 matched_since_deviation = 0
78
79 if deviations > deviation_threshold:
80 break
81
82 if debug:
83 output = (
84 f"First (up to {last_matched_i}): {first[:last_matched_i]!r}\n"
85 "\n"
86 f"Second (up to {last_matched_j}): {second[:last_matched_j]!r}\n"
87 "\n"
88 f"Current deviation: {deviations}\n"
89 f"Matched since last deviation: {matched_since_deviation}\n"
90 + "-" * 40 + "\n"
91 )
92 sys.stdout.write("\r" + output)
93 sys.stdout.flush()
94 time.sleep(0.01) # Add a short delay for readability (optional)
95
96 # Return the last matched positions instead of the current indices
97 return last_matched_i, last_matched_j
98
99 def format_key(key: str) -> str:
100 """Format a key string to be more readable.
101 Converts camelCase and snake_case to Title Case with spaces."""
102 # First replace non-alphanumeric with spaces
103 result = ''.join(' ' if not c.isalnum() else c for c in key)
104
105 # Handle camelCase
106 formatted = ''
107 for i, c in enumerate(result):
108 if i > 0 and c.isupper() and result[i-1].islower():
109 formatted += ' ' + c
110 else:
111 formatted += c
112
113 # Split on spaces and capitalize each word
114 return ' '.join(word.capitalize() for word in formatted.split())
115
116 def dict_to_text(d: dict) -> str:
117 parts = []
118 for key, value in d.items():
119 parts.append(f"{format_key(str(key))}:")
120 parts.append(f"{value}")
121 parts.append("") # Add empty line between entries
122
123 return "\n".join(parts).rstrip() # rstrip to remove trailing newline
124
125 def truncate_text(text: str, length: int, at_end: bool = True, replacement: str = "...") -> str:
126 orig_length = len(text)
127 if orig_length <= length:
128 return text
129 if at_end:
130 return text[:length] + replacement
131 else:
132 return replacement + text[-length:]
133
134 def truncate_text_by_ratio(text: str, threshold: int, replacement: str = "...", ratio: float = 0.5) -> str:
135 """Truncate text with replacement at a specified ratio position."""
136 threshold = int(threshold)
137 if not threshold or len(text) <= threshold:
138 return text
139
140 # Clamp ratio to valid range
141 ratio = max(0.0, min(1.0, float(ratio)))
142
143 # Calculate available space for original text after accounting for replacement
144 available_space = threshold - len(replacement)
145 if available_space <= 0:
146 return replacement[:threshold]
147
148 # Handle edge cases for efficiency
149 if ratio == 0.0:
150 # Replace from start: "...text"
151 return replacement + text[-available_space:]
152 elif ratio == 1.0:
153 # Replace from end: "text..."
154 return text[:available_space] + replacement
155 else:
156 # Replace in middle based on ratio
157 start_len = int(available_space * ratio)
158 end_len = available_space - start_len
159 return text[:start_len] + replacement + text[-end_len:]
160
161
162 def replace_file_includes(text: str, placeholder_pattern: str = r"§§include\(([^)]+)\)") -> str:
163 # Replace include aliases with file content
164 if not text:
165 return text
166
167 def _repl(match):
168 path = match.group(1)
169 try:
170 # read file content
171 path = files.fix_dev_path(path)
172 return files.read_file(path)
173 except Exception:
174 # if file not readable keep original placeholder
175 return match.group(0)
176
177 return re.sub(placeholder_pattern, _repl, text)