Skip to main content

alef_e2e/
escape.rs

1//! Language-specific string escaping for e2e test code generation.
2
3/// Escape a string for embedding in a Python string literal.
4pub fn escape_python(s: &str) -> String {
5    s.replace('\\', "\\\\")
6        .replace('"', "\\\"")
7        .replace('\n', "\\n")
8        .replace('\r', "\\r")
9        .replace('\t', "\\t")
10}
11
12/// Escape a string for embedding in a Rust string literal.
13pub fn escape_rust(s: &str) -> String {
14    s.replace('\\', "\\\\")
15        .replace('"', "\\\"")
16        .replace('\n', "\\n")
17        .replace('\r', "\\r")
18        .replace('\t', "\\t")
19}
20
21/// Compute the number of # needed for a Rust raw string literal.
22pub fn raw_string_hashes(s: &str) -> usize {
23    let mut max_hashes = 0;
24    let mut current = 0;
25    let mut after_quote = false;
26    for ch in s.chars() {
27        if ch == '"' {
28            after_quote = true;
29            current = 0;
30        } else if ch == '#' && after_quote {
31            current += 1;
32            max_hashes = max_hashes.max(current);
33        } else {
34            after_quote = false;
35            current = 0;
36        }
37    }
38    max_hashes + 1
39}
40
41/// Format a string as a Rust raw string literal (r#"..."#).
42pub fn rust_raw_string(s: &str) -> String {
43    let hashes = raw_string_hashes(s);
44    let h: String = "#".repeat(hashes);
45    format!("r{h}\"{s}\"{h}")
46}
47
48/// Escape a string for embedding in a JavaScript/TypeScript double-quoted string literal.
49///
50/// `$` does not need escaping in double-quoted strings (only in template literals).
51/// Escaping it would produce `\$` which Biome flags as `noUselessEscapeInString`.
52pub fn escape_js(s: &str) -> String {
53    s.replace('\\', "\\\\")
54        .replace('"', "\\\"")
55        .replace('\n', "\\n")
56        .replace('\r', "\\r")
57        .replace('\t', "\\t")
58}
59
60/// Escape a string for embedding in a JavaScript/TypeScript template literal (backtick string).
61///
62/// Template literals interpolate `${...}` and use backtick delimiters, so both
63/// `` ` `` and `$` must be escaped to prevent unintended interpolation.
64pub fn escape_js_template(s: &str) -> String {
65    s.replace('\\', "\\\\").replace('`', "\\`").replace('$', "\\$")
66}
67
68/// Format a string as a Go string literal (backtick or quoted).
69pub fn go_string_literal(s: &str) -> String {
70    if !s.contains('`') {
71        format!("`{s}`")
72    } else {
73        format!("\"{}\"", escape_go(s))
74    }
75}
76
77/// Escape a string for embedding in a Go double-quoted string.
78pub fn escape_go(s: &str) -> String {
79    s.replace('\\', "\\\\")
80        .replace('"', "\\\"")
81        .replace('\n', "\\n")
82        .replace('\r', "\\r")
83        .replace('\t', "\\t")
84}
85
86/// Escape a string for embedding in a Java string literal.
87pub fn escape_java(s: &str) -> String {
88    s.replace('\\', "\\\\")
89        .replace('"', "\\\"")
90        .replace('\n', "\\n")
91        .replace('\r', "\\r")
92        .replace('\t', "\\t")
93}
94
95/// Escape a string for embedding in a C# string literal.
96pub fn escape_csharp(s: &str) -> String {
97    s.replace('\\', "\\\\")
98        .replace('"', "\\\"")
99        .replace('\n', "\\n")
100        .replace('\r', "\\r")
101        .replace('\t', "\\t")
102}
103
104/// Escape a string for embedding in a PHP string literal.
105pub fn escape_php(s: &str) -> String {
106    s.replace('\\', "\\\\")
107        .replace('"', "\\\"")
108        .replace('$', "\\$")
109        .replace('\n', "\\n")
110        .replace('\r', "\\r")
111        .replace('\t', "\\t")
112}
113
114/// Escape a string for embedding in a double-quoted Ruby string literal.
115pub fn escape_ruby(s: &str) -> String {
116    s.replace('\\', "\\\\")
117        .replace('"', "\\\"")
118        .replace('#', "\\#")
119        .replace('\n', "\\n")
120        .replace('\r', "\\r")
121        .replace('\t', "\\t")
122}
123
124/// Escape a string for embedding in a single-quoted Ruby string literal.
125/// Single-quoted Ruby strings only interpret `\\` and `\'`.
126pub fn escape_ruby_single(s: &str) -> String {
127    s.replace('\\', "\\\\").replace('\'', "\\'")
128}
129
130/// Returns true if the string needs double quotes (contains control characters
131/// that require escape sequences only available in double-quoted strings).
132pub fn ruby_needs_double_quotes(s: &str) -> bool {
133    s.contains('\n') || s.contains('\r') || s.contains('\t') || s.contains('\0')
134}
135
136/// Format a string as a Ruby literal, preferring single quotes.
137pub fn ruby_string_literal(s: &str) -> String {
138    if ruby_needs_double_quotes(s) {
139        format!("\"{}\"", escape_ruby(s))
140    } else {
141        format!("'{}'", escape_ruby_single(s))
142    }
143}
144
145/// Escape a string for embedding in an Elixir string literal.
146pub fn escape_elixir(s: &str) -> String {
147    s.replace('\\', "\\\\")
148        .replace('"', "\\\"")
149        .replace('#', "\\#")
150        .replace('\n', "\\n")
151        .replace('\r', "\\r")
152        .replace('\t', "\\t")
153}
154
155/// Escape a string for embedding in an R string literal.
156pub fn escape_r(s: &str) -> String {
157    s.replace('\\', "\\\\")
158        .replace('"', "\\\"")
159        .replace('\n', "\\n")
160        .replace('\r', "\\r")
161        .replace('\t', "\\t")
162}
163
164/// Escape a string for embedding in a C string literal.
165pub fn escape_c(s: &str) -> String {
166    s.replace('\\', "\\\\")
167        .replace('"', "\\\"")
168        .replace('\n', "\\n")
169        .replace('\r', "\\r")
170        .replace('\t', "\\t")
171}
172
173/// Sanitize an identifier for use as a test function name.
174/// Replaces non-alphanumeric characters with underscores, strips leading digits.
175pub fn sanitize_ident(s: &str) -> String {
176    let mut result = String::with_capacity(s.len());
177    for ch in s.chars() {
178        if ch.is_ascii_alphanumeric() || ch == '_' {
179            result.push(ch);
180        } else {
181            result.push('_');
182        }
183    }
184    // Strip leading digits
185    let trimmed = result.trim_start_matches(|c: char| c.is_ascii_digit());
186    if trimmed.is_empty() {
187        "_".to_string()
188    } else {
189        trimmed.to_string()
190    }
191}
192
193/// Convert a category name to a sanitized filename component.
194pub fn sanitize_filename(s: &str) -> String {
195    s.chars()
196        .map(|c| if c.is_ascii_alphanumeric() || c == '_' { c } else { '_' })
197        .collect::<String>()
198        .to_lowercase()
199}
200
201/// Expand fixture template expressions in a string value.
202///
203/// Supported templates:
204/// - `{{ repeat 'X' N times }}` — expands to the character X repeated N times
205///
206/// If no templates are found, the original string is returned unchanged.
207pub fn expand_fixture_templates(s: &str) -> String {
208    const PREFIX: &str = "{{ repeat '";
209    const SUFFIX: &str = " times }}";
210
211    let mut result = String::with_capacity(s.len());
212    let mut remaining = s;
213
214    while let Some(start) = remaining.find(PREFIX) {
215        result.push_str(&remaining[..start]);
216        let after_prefix = &remaining[start + PREFIX.len()..];
217
218        // Expect character(s) followed by `' N times }}`
219        if let Some(quote_pos) = after_prefix.find("' ") {
220            let ch = &after_prefix[..quote_pos];
221            let after_quote = &after_prefix[quote_pos + 2..];
222
223            if let Some(end) = after_quote.find(SUFFIX) {
224                let count_str = after_quote[..end].trim();
225                if let Ok(count) = count_str.parse::<usize>() {
226                    result.push_str(&ch.repeat(count));
227                    remaining = &after_quote[end + SUFFIX.len()..];
228                    continue;
229                }
230            }
231        }
232
233        // Template didn't match — emit the prefix literally and continue
234        result.push_str(PREFIX);
235        remaining = after_prefix;
236    }
237    result.push_str(remaining);
238    result
239}
240
241/// Escape a string for embedding in a POSIX single-quoted shell string literal.
242///
243/// Wraps the string in single quotes and escapes embedded single quotes as `'\''`.
244/// Single-quoted shell strings treat every character literally except `'`, so
245/// no other escaping is needed.
246pub fn escape_shell(s: &str) -> String {
247    s.replace('\'', r"'\''")
248}
249
250/// Escape a string for embedding in a Gleam string literal.
251pub fn escape_gleam(s: &str) -> String {
252    s.replace('\\', "\\\\")
253        .replace('"', "\\\"")
254        .replace('\n', "\\n")
255        .replace('\r', "\\r")
256        .replace('\t', "\\t")
257}
258
259/// Escape a string for embedding in a Zig string literal.
260pub fn escape_zig(s: &str) -> String {
261    s.replace('\\', "\\\\")
262        .replace('"', "\\\"")
263        .replace('\n', "\\n")
264        .replace('\r', "\\r")
265        .replace('\t', "\\t")
266}