1pub 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
12pub 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
21pub 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
41pub 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
48pub 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
60pub fn escape_js_template(s: &str) -> String {
65 s.replace('\\', "\\\\").replace('`', "\\`").replace('$', "\\$")
66}
67
68fn go_needs_quoted(s: &str) -> bool {
74 s.contains('`') || s.bytes().any(|b| b == 0 || b == b'\r')
75}
76
77pub fn go_string_literal(s: &str) -> String {
83 if go_needs_quoted(s) {
84 format!("\"{}\"", escape_go(s))
85 } else {
86 format!("`{s}`")
87 }
88}
89
90pub fn escape_go(s: &str) -> String {
96 let mut out = String::with_capacity(s.len());
97 for b in s.bytes() {
98 match b {
99 b'\\' => out.push_str("\\\\"),
100 b'"' => out.push_str("\\\""),
101 b'\n' => out.push_str("\\n"),
102 b'\r' => out.push_str("\\r"),
103 b'\t' => out.push_str("\\t"),
104 0 => out.push_str("\\x00"),
105 b if b < 0x20 || b == 0x7f => {
107 out.push_str(&format!("\\x{b:02x}"));
108 }
109 _ => out.push(b as char),
110 }
111 }
112 out
113}
114
115pub fn escape_java(s: &str) -> String {
117 s.replace('\\', "\\\\")
118 .replace('"', "\\\"")
119 .replace('\n', "\\n")
120 .replace('\r', "\\r")
121 .replace('\t', "\\t")
122}
123
124pub fn escape_csharp(s: &str) -> String {
126 s.replace('\\', "\\\\")
127 .replace('"', "\\\"")
128 .replace('\n', "\\n")
129 .replace('\r', "\\r")
130 .replace('\t', "\\t")
131}
132
133pub fn escape_php(s: &str) -> String {
135 s.replace('\\', "\\\\")
136 .replace('"', "\\\"")
137 .replace('$', "\\$")
138 .replace('\n', "\\n")
139 .replace('\r', "\\r")
140 .replace('\t', "\\t")
141}
142
143pub fn escape_ruby(s: &str) -> String {
145 s.replace('\\', "\\\\")
146 .replace('"', "\\\"")
147 .replace('#', "\\#")
148 .replace('\n', "\\n")
149 .replace('\r', "\\r")
150 .replace('\t', "\\t")
151}
152
153pub fn escape_ruby_single(s: &str) -> String {
156 s.replace('\\', "\\\\").replace('\'', "\\'")
157}
158
159pub fn ruby_needs_double_quotes(s: &str) -> bool {
162 s.contains('\n') || s.contains('\r') || s.contains('\t') || s.contains('\0')
163}
164
165pub fn ruby_string_literal(s: &str) -> String {
167 if ruby_needs_double_quotes(s) {
168 format!("\"{}\"", escape_ruby(s))
169 } else {
170 format!("'{}'", escape_ruby_single(s))
171 }
172}
173
174pub fn escape_elixir(s: &str) -> String {
176 s.replace('\\', "\\\\")
177 .replace('"', "\\\"")
178 .replace('#', "\\#")
179 .replace('\n', "\\n")
180 .replace('\r', "\\r")
181 .replace('\t', "\\t")
182}
183
184pub fn escape_r(s: &str) -> String {
186 s.replace('\\', "\\\\")
187 .replace('"', "\\\"")
188 .replace('\n', "\\n")
189 .replace('\r', "\\r")
190 .replace('\t', "\\t")
191}
192
193pub fn escape_c(s: &str) -> String {
195 s.replace('\\', "\\\\")
196 .replace('"', "\\\"")
197 .replace('\n', "\\n")
198 .replace('\r', "\\r")
199 .replace('\t', "\\t")
200}
201
202pub fn sanitize_ident(s: &str) -> String {
205 let mut result = String::with_capacity(s.len());
206 for ch in s.chars() {
207 if ch.is_ascii_alphanumeric() || ch == '_' {
208 result.push(ch);
209 } else {
210 result.push('_');
211 }
212 }
213 let trimmed = result.trim_start_matches(|c: char| c.is_ascii_digit());
215 if trimmed.is_empty() {
216 "_".to_string()
217 } else {
218 trimmed.to_string()
219 }
220}
221
222pub fn sanitize_filename(s: &str) -> String {
224 s.chars()
225 .map(|c| if c.is_ascii_alphanumeric() || c == '_' { c } else { '_' })
226 .collect::<String>()
227 .to_lowercase()
228}
229
230pub fn expand_fixture_templates(s: &str) -> String {
237 const PREFIX: &str = "{{ repeat '";
238 const SUFFIX: &str = " times }}";
239
240 let mut result = String::with_capacity(s.len());
241 let mut remaining = s;
242
243 while let Some(start) = remaining.find(PREFIX) {
244 result.push_str(&remaining[..start]);
245 let after_prefix = &remaining[start + PREFIX.len()..];
246
247 if let Some(quote_pos) = after_prefix.find("' ") {
249 let ch = &after_prefix[..quote_pos];
250 let after_quote = &after_prefix[quote_pos + 2..];
251
252 if let Some(end) = after_quote.find(SUFFIX) {
253 let count_str = after_quote[..end].trim();
254 if let Ok(count) = count_str.parse::<usize>() {
255 result.push_str(&ch.repeat(count));
256 remaining = &after_quote[end + SUFFIX.len()..];
257 continue;
258 }
259 }
260 }
261
262 result.push_str(PREFIX);
264 remaining = after_prefix;
265 }
266 result.push_str(remaining);
267 result
268}
269
270pub fn escape_shell(s: &str) -> String {
276 s.replace('\'', r"'\''")
277}
278
279pub fn escape_gleam(s: &str) -> String {
281 s.replace('\\', "\\\\")
282 .replace('"', "\\\"")
283 .replace('\n', "\\n")
284 .replace('\r', "\\r")
285 .replace('\t', "\\t")
286}
287
288pub fn escape_zig(s: &str) -> String {
290 s.replace('\\', "\\\\")
291 .replace('"', "\\\"")
292 .replace('\n', "\\n")
293 .replace('\r', "\\r")
294 .replace('\t', "\\t")
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300
301 #[test]
304 fn go_string_literal_nul_bytes_use_quoted_form() {
305 let s = "Hello\x00World";
306 let lit = go_string_literal(s);
307 assert!(
309 !lit.as_bytes().contains(&0u8),
310 "go_string_literal emitted a NUL byte — gofmt would reject this: {lit:?}"
311 );
312 assert!(
314 lit.starts_with('"'),
315 "expected double-quoted string for NUL input, got: {lit:?}"
316 );
317 assert!(
319 lit.contains("\\x00"),
320 "expected \\x00 escape sequence for NUL byte, got: {lit:?}"
321 );
322 }
323
324 #[test]
327 fn go_string_literal_carriage_return_uses_quoted_form() {
328 let s = "line1\r\nline2";
329 let lit = go_string_literal(s);
330 assert!(
331 !lit.as_bytes().contains(&b'\r'),
332 "go_string_literal emitted a literal CR — gofmt would reject this: {lit:?}"
333 );
334 assert!(
335 lit.starts_with('"'),
336 "expected double-quoted string for CR input, got: {lit:?}"
337 );
338 }
339
340 #[test]
343 fn go_string_literal_plain_string_uses_backtick() {
344 let s = "Hello World\nwith newline";
345 let lit = go_string_literal(s);
346 assert!(
347 lit.starts_with('`'),
348 "expected backtick form for plain string, got: {lit:?}"
349 );
350 }
351
352 #[test]
354 fn go_string_literal_backtick_in_string_uses_quoted_form() {
355 let s = "has `backtick`";
356 let lit = go_string_literal(s);
357 assert!(
358 lit.starts_with('"'),
359 "expected double-quoted form when string contains backtick, got: {lit:?}"
360 );
361 }
362}