codex_external_agent_migration/
rewrite.rs1#[derive(Clone, Copy)]
3pub struct RewriteProfile {
4 doc_file_name: &'static str,
5 term_variants: &'static [&'static str],
6 case_sensitive_term_variants: &'static [&'static str],
7}
8
9impl RewriteProfile {
10 pub const fn new(doc_file_name: &'static str, term_variants: &'static [&'static str]) -> Self {
11 Self {
12 doc_file_name,
13 term_variants,
14 case_sensitive_term_variants: &[],
15 }
16 }
17
18 pub const fn with_case_sensitive_term_variants(
19 mut self,
20 term_variants: &'static [&'static str],
21 ) -> Self {
22 self.case_sensitive_term_variants = term_variants;
23 self
24 }
25
26 pub const fn doc_file_name(self) -> &'static str {
27 self.doc_file_name
28 }
29
30 pub const fn term_variants(self) -> &'static [&'static str] {
31 self.term_variants
32 }
33
34 pub const fn case_sensitive_term_variants(self) -> &'static [&'static str] {
35 self.case_sensitive_term_variants
36 }
37
38 pub fn rewrite(self, content: &str) -> String {
40 let mut rewritten =
41 replace_case_insensitive_with_boundaries(content, self.doc_file_name, "AGENTS.md");
42 for from in self.term_variants {
43 rewritten = replace_case_insensitive_with_boundaries(&rewritten, from, "Codex");
44 }
45 for from in self.case_sensitive_term_variants {
46 rewritten = replace_with_boundaries(&rewritten, from, "Codex");
47 }
48 rewritten
49 }
50}
51
52fn replace_with_boundaries(input: &str, needle: &str, replacement: &str) -> String {
53 if needle.is_empty() {
54 return input.to_string();
55 }
56
57 let bytes = input.as_bytes();
58 let mut output = String::with_capacity(input.len());
59 let mut last_emitted = 0usize;
60 let mut search_start = 0usize;
61
62 while let Some(relative_pos) = input[search_start..].find(needle) {
63 let start = search_start + relative_pos;
64 let end = start + needle.len();
65 let boundary_before = start == 0 || !is_word_byte(bytes[start - 1]);
66 let boundary_after = end == bytes.len() || !is_word_byte(bytes[end]);
67
68 if boundary_before && boundary_after {
69 output.push_str(&input[last_emitted..start]);
70 output.push_str(replacement);
71 last_emitted = end;
72 }
73
74 search_start = end;
75 }
76
77 if last_emitted == 0 {
78 return input.to_string();
79 }
80
81 output.push_str(&input[last_emitted..]);
82 output
83}
84
85fn replace_case_insensitive_with_boundaries(
86 input: &str,
87 needle: &str,
88 replacement: &str,
89) -> String {
90 let needle_lower = needle.to_ascii_lowercase();
91 if needle_lower.is_empty() {
92 return input.to_string();
93 }
94
95 let haystack_lower = input.to_ascii_lowercase();
96 let bytes = input.as_bytes();
97 let mut output = String::with_capacity(input.len());
98 let mut last_emitted = 0usize;
99 let mut search_start = 0usize;
100
101 while let Some(relative_pos) = haystack_lower[search_start..].find(&needle_lower) {
102 let start = search_start + relative_pos;
103 let end = start + needle_lower.len();
104 let boundary_before = start == 0 || !is_word_byte(bytes[start - 1]);
105 let boundary_after = end == bytes.len() || !is_word_byte(bytes[end]);
106
107 if boundary_before && boundary_after {
108 output.push_str(&input[last_emitted..start]);
109 output.push_str(replacement);
110 last_emitted = end;
111 }
112
113 search_start = start + 1;
114 }
115
116 if last_emitted == 0 {
117 return input.to_string();
118 }
119
120 output.push_str(&input[last_emitted..]);
121 output
122}
123
124fn is_word_byte(byte: u8) -> bool {
125 byte.is_ascii_alphanumeric() || byte == b'_'
126}
127
128#[cfg(test)]
129#[path = "rewrite_tests.rs"]
130mod tests;