1pub fn escape_python(s: &str) -> String {
5 let mut out = String::with_capacity(s.len());
6 for ch in s.chars() {
7 match ch {
8 '\\' => out.push_str("\\\\"),
9 '"' => out.push_str("\\\""),
10 '\n' => out.push_str("\\n"),
11 '\r' => out.push_str("\\r"),
12 '\t' => out.push_str("\\t"),
13 c if (c as u32) < 0x20 => {
14 out.push_str(&format!("\\x{:02x}", c as u32));
16 }
17 c => out.push(c),
18 }
19 }
20 out
21}
22
23pub fn escape_rust(s: &str) -> String {
25 s.replace('\\', "\\\\")
26 .replace('"', "\\\"")
27 .replace('\n', "\\n")
28 .replace('\r', "\\r")
29 .replace('\t', "\\t")
30}
31
32pub fn raw_string_hashes(s: &str) -> usize {
34 let mut max_hashes = 0;
35 let mut current = 0;
36 let mut after_quote = false;
37 for ch in s.chars() {
38 if ch == '"' {
39 after_quote = true;
40 current = 0;
41 } else if ch == '#' && after_quote {
42 current += 1;
43 max_hashes = max_hashes.max(current);
44 } else {
45 after_quote = false;
46 current = 0;
47 }
48 }
49 max_hashes + 1
50}
51
52pub fn rust_raw_string(s: &str) -> String {
54 let hashes = raw_string_hashes(s);
55 let h: String = "#".repeat(hashes);
56 format!("r{h}\"{s}\"{h}")
57}
58
59pub fn escape_js(s: &str) -> String {
64 s.replace('\\', "\\\\")
65 .replace('"', "\\\"")
66 .replace('\n', "\\n")
67 .replace('\r', "\\r")
68 .replace('\t', "\\t")
69}
70
71pub fn escape_js_template(s: &str) -> String {
76 s.replace('\\', "\\\\").replace('`', "\\`").replace('$', "\\$")
77}
78
79fn go_needs_quoted(s: &str) -> bool {
85 s.contains('`') || s.bytes().any(|b| b == 0 || b == b'\r')
86}
87
88pub fn go_string_literal(s: &str) -> String {
94 if go_needs_quoted(s) {
95 format!("\"{}\"", escape_go(s))
96 } else {
97 format!("`{s}`")
98 }
99}
100
101pub fn escape_go(s: &str) -> String {
107 let mut out = String::with_capacity(s.len());
108 for b in s.bytes() {
109 match b {
110 b'\\' => out.push_str("\\\\"),
111 b'"' => out.push_str("\\\""),
112 b'\n' => out.push_str("\\n"),
113 b'\r' => out.push_str("\\r"),
114 b'\t' => out.push_str("\\t"),
115 0 => out.push_str("\\x00"),
116 b if b < 0x20 || b == 0x7f => {
118 out.push_str(&format!("\\x{b:02x}"));
119 }
120 _ => out.push(b as char),
121 }
122 }
123 out
124}
125
126pub fn escape_java(s: &str) -> String {
128 s.replace('\\', "\\\\")
129 .replace('"', "\\\"")
130 .replace('\n', "\\n")
131 .replace('\r', "\\r")
132 .replace('\t', "\\t")
133}
134
135pub fn escape_kotlin(s: &str) -> String {
138 s.replace('\\', "\\\\")
139 .replace('"', "\\\"")
140 .replace('$', "\\$")
141 .replace('\n', "\\n")
142 .replace('\r', "\\r")
143 .replace('\t', "\\t")
144}
145
146pub fn escape_csharp(s: &str) -> String {
148 s.replace('\\', "\\\\")
149 .replace('"', "\\\"")
150 .replace('\n', "\\n")
151 .replace('\r', "\\r")
152 .replace('\t', "\\t")
153}
154
155pub fn escape_php(s: &str) -> String {
157 s.replace('\\', "\\\\")
158 .replace('"', "\\\"")
159 .replace('$', "\\$")
160 .replace('\n', "\\n")
161 .replace('\r', "\\r")
162 .replace('\t', "\\t")
163}
164
165pub fn escape_ruby(s: &str) -> String {
167 s.replace('\\', "\\\\")
168 .replace('"', "\\\"")
169 .replace('#', "\\#")
170 .replace('\n', "\\n")
171 .replace('\r', "\\r")
172 .replace('\t', "\\t")
173}
174
175pub fn escape_ruby_single(s: &str) -> String {
178 s.replace('\\', "\\\\").replace('\'', "\\'")
179}
180
181pub fn ruby_needs_double_quotes(s: &str) -> bool {
184 s.contains('\n') || s.contains('\r') || s.contains('\t') || s.contains('\0')
185}
186
187pub fn ruby_string_literal(s: &str) -> String {
189 if ruby_needs_double_quotes(s) {
190 format!("\"{}\"", escape_ruby(s))
191 } else {
192 format!("'{}'", escape_ruby_single(s))
193 }
194}
195
196pub fn escape_elixir(s: &str) -> String {
198 s.replace('\\', "\\\\")
199 .replace('"', "\\\"")
200 .replace('#', "\\#")
201 .replace('\n', "\\n")
202 .replace('\r', "\\r")
203 .replace('\t', "\\t")
204}
205
206pub fn escape_r(s: &str) -> String {
208 s.replace('\\', "\\\\")
209 .replace('"', "\\\"")
210 .replace('\n', "\\n")
211 .replace('\r', "\\r")
212 .replace('\t', "\\t")
213}
214
215pub fn escape_c(s: &str) -> String {
217 s.replace('\\', "\\\\")
218 .replace('"', "\\\"")
219 .replace('\n', "\\n")
220 .replace('\r', "\\r")
221 .replace('\t', "\\t")
222}
223
224pub fn sanitize_ident(s: &str) -> String {
227 let mut result = String::with_capacity(s.len());
228 for ch in s.chars() {
229 if ch.is_ascii_alphanumeric() || ch == '_' {
230 result.push(ch);
231 } else {
232 result.push('_');
233 }
234 }
235 let trimmed = result.trim_start_matches(|c: char| c.is_ascii_digit());
237 if trimmed.is_empty() {
238 "_".to_string()
239 } else {
240 trimmed.to_string()
241 }
242}
243
244pub fn sanitize_filename(s: &str) -> String {
246 s.chars()
247 .map(|c| if c.is_ascii_alphanumeric() || c == '_' { c } else { '_' })
248 .collect::<String>()
249 .to_lowercase()
250}
251
252pub fn expand_fixture_templates(s: &str) -> String {
259 const PREFIX: &str = "{{ repeat '";
260 const SUFFIX: &str = " times }}";
261
262 let mut result = String::with_capacity(s.len());
263 let mut remaining = s;
264
265 while let Some(start) = remaining.find(PREFIX) {
266 result.push_str(&remaining[..start]);
267 let after_prefix = &remaining[start + PREFIX.len()..];
268
269 if let Some(quote_pos) = after_prefix.find("' ") {
271 let ch = &after_prefix[..quote_pos];
272 let after_quote = &after_prefix[quote_pos + 2..];
273
274 if let Some(end) = after_quote.find(SUFFIX) {
275 let count_str = after_quote[..end].trim();
276 if let Ok(count) = count_str.parse::<usize>() {
277 result.push_str(&ch.repeat(count));
278 remaining = &after_quote[end + SUFFIX.len()..];
279 continue;
280 }
281 }
282 }
283
284 result.push_str(PREFIX);
286 remaining = after_prefix;
287 }
288 result.push_str(remaining);
289 result
290}
291
292pub fn escape_shell(s: &str) -> String {
298 s.replace('\'', r"'\''")
299}
300
301pub fn escape_gleam(s: &str) -> String {
303 s.replace('\\', "\\\\")
304 .replace('"', "\\\"")
305 .replace('\n', "\\n")
306 .replace('\r', "\\r")
307 .replace('\t', "\\t")
308}
309
310pub fn escape_zig(s: &str) -> String {
312 s.replace('\\', "\\\\")
313 .replace('"', "\\\"")
314 .replace('\n', "\\n")
315 .replace('\r', "\\r")
316 .replace('\t', "\\t")
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322
323 #[test]
326 fn go_string_literal_nul_bytes_use_quoted_form() {
327 let s = "Hello\x00World";
328 let lit = go_string_literal(s);
329 assert!(
331 !lit.as_bytes().contains(&0u8),
332 "go_string_literal emitted a NUL byte — gofmt would reject this: {lit:?}"
333 );
334 assert!(
336 lit.starts_with('"'),
337 "expected double-quoted string for NUL input, got: {lit:?}"
338 );
339 assert!(
341 lit.contains("\\x00"),
342 "expected \\x00 escape sequence for NUL byte, got: {lit:?}"
343 );
344 }
345
346 #[test]
349 fn go_string_literal_carriage_return_uses_quoted_form() {
350 let s = "line1\r\nline2";
351 let lit = go_string_literal(s);
352 assert!(
353 !lit.as_bytes().contains(&b'\r'),
354 "go_string_literal emitted a literal CR — gofmt would reject this: {lit:?}"
355 );
356 assert!(
357 lit.starts_with('"'),
358 "expected double-quoted string for CR input, got: {lit:?}"
359 );
360 }
361
362 #[test]
365 fn go_string_literal_plain_string_uses_backtick() {
366 let s = "Hello World\nwith newline";
367 let lit = go_string_literal(s);
368 assert!(
369 lit.starts_with('`'),
370 "expected backtick form for plain string, got: {lit:?}"
371 );
372 }
373
374 #[test]
376 fn go_string_literal_backtick_in_string_uses_quoted_form() {
377 let s = "has `backtick`";
378 let lit = go_string_literal(s);
379 assert!(
380 lit.starts_with('"'),
381 "expected double-quoted form when string contains backtick, got: {lit:?}"
382 );
383 }
384}