Skip to main content

codex_external_agent_migration/
rewrite.rs

1/// Describes source-specific terms that should be rewritten in migrated artifacts.
2#[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    /// Rewrites source-specific documentation names and product terms to their Codex forms.
39    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;