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_kotlin(s: &str) -> String {
127 s.replace('\\', "\\\\")
128 .replace('"', "\\\"")
129 .replace('$', "\\$")
130 .replace('\n', "\\n")
131 .replace('\r', "\\r")
132 .replace('\t', "\\t")
133}
134
135pub fn escape_csharp(s: &str) -> String {
137 s.replace('\\', "\\\\")
138 .replace('"', "\\\"")
139 .replace('\n', "\\n")
140 .replace('\r', "\\r")
141 .replace('\t', "\\t")
142}
143
144pub fn escape_php(s: &str) -> String {
146 s.replace('\\', "\\\\")
147 .replace('"', "\\\"")
148 .replace('$', "\\$")
149 .replace('\n', "\\n")
150 .replace('\r', "\\r")
151 .replace('\t', "\\t")
152}
153
154pub fn escape_ruby(s: &str) -> String {
156 s.replace('\\', "\\\\")
157 .replace('"', "\\\"")
158 .replace('#', "\\#")
159 .replace('\n', "\\n")
160 .replace('\r', "\\r")
161 .replace('\t', "\\t")
162}
163
164pub fn escape_ruby_single(s: &str) -> String {
167 s.replace('\\', "\\\\").replace('\'', "\\'")
168}
169
170pub fn ruby_needs_double_quotes(s: &str) -> bool {
173 s.contains('\n') || s.contains('\r') || s.contains('\t') || s.contains('\0')
174}
175
176pub fn ruby_string_literal(s: &str) -> String {
178 if ruby_needs_double_quotes(s) {
179 format!("\"{}\"", escape_ruby(s))
180 } else {
181 format!("'{}'", escape_ruby_single(s))
182 }
183}
184
185pub fn escape_elixir(s: &str) -> String {
187 s.replace('\\', "\\\\")
188 .replace('"', "\\\"")
189 .replace('#', "\\#")
190 .replace('\n', "\\n")
191 .replace('\r', "\\r")
192 .replace('\t', "\\t")
193}
194
195pub fn escape_r(s: &str) -> String {
197 s.replace('\\', "\\\\")
198 .replace('"', "\\\"")
199 .replace('\n', "\\n")
200 .replace('\r', "\\r")
201 .replace('\t', "\\t")
202}
203
204pub fn escape_c(s: &str) -> String {
206 s.replace('\\', "\\\\")
207 .replace('"', "\\\"")
208 .replace('\n', "\\n")
209 .replace('\r', "\\r")
210 .replace('\t', "\\t")
211}
212
213pub fn sanitize_ident(s: &str) -> String {
216 let mut result = String::with_capacity(s.len());
217 for ch in s.chars() {
218 if ch.is_ascii_alphanumeric() || ch == '_' {
219 result.push(ch);
220 } else {
221 result.push('_');
222 }
223 }
224 let trimmed = result.trim_start_matches(|c: char| c.is_ascii_digit());
226 if trimmed.is_empty() {
227 "_".to_string()
228 } else {
229 trimmed.to_string()
230 }
231}
232
233pub fn sanitize_filename(s: &str) -> String {
235 s.chars()
236 .map(|c| if c.is_ascii_alphanumeric() || c == '_' { c } else { '_' })
237 .collect::<String>()
238 .to_lowercase()
239}
240
241pub fn expand_fixture_templates(s: &str) -> String {
248 const PREFIX: &str = "{{ repeat '";
249 const SUFFIX: &str = " times }}";
250
251 let mut result = String::with_capacity(s.len());
252 let mut remaining = s;
253
254 while let Some(start) = remaining.find(PREFIX) {
255 result.push_str(&remaining[..start]);
256 let after_prefix = &remaining[start + PREFIX.len()..];
257
258 if let Some(quote_pos) = after_prefix.find("' ") {
260 let ch = &after_prefix[..quote_pos];
261 let after_quote = &after_prefix[quote_pos + 2..];
262
263 if let Some(end) = after_quote.find(SUFFIX) {
264 let count_str = after_quote[..end].trim();
265 if let Ok(count) = count_str.parse::<usize>() {
266 result.push_str(&ch.repeat(count));
267 remaining = &after_quote[end + SUFFIX.len()..];
268 continue;
269 }
270 }
271 }
272
273 result.push_str(PREFIX);
275 remaining = after_prefix;
276 }
277 result.push_str(remaining);
278 result
279}
280
281pub fn escape_shell(s: &str) -> String {
287 s.replace('\'', r"'\''")
288}
289
290pub fn escape_gleam(s: &str) -> String {
292 s.replace('\\', "\\\\")
293 .replace('"', "\\\"")
294 .replace('\n', "\\n")
295 .replace('\r', "\\r")
296 .replace('\t', "\\t")
297}
298
299pub fn escape_zig(s: &str) -> String {
301 s.replace('\\', "\\\\")
302 .replace('"', "\\\"")
303 .replace('\n', "\\n")
304 .replace('\r', "\\r")
305 .replace('\t', "\\t")
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 #[test]
315 fn go_string_literal_nul_bytes_use_quoted_form() {
316 let s = "Hello\x00World";
317 let lit = go_string_literal(s);
318 assert!(
320 !lit.as_bytes().contains(&0u8),
321 "go_string_literal emitted a NUL byte — gofmt would reject this: {lit:?}"
322 );
323 assert!(
325 lit.starts_with('"'),
326 "expected double-quoted string for NUL input, got: {lit:?}"
327 );
328 assert!(
330 lit.contains("\\x00"),
331 "expected \\x00 escape sequence for NUL byte, got: {lit:?}"
332 );
333 }
334
335 #[test]
338 fn go_string_literal_carriage_return_uses_quoted_form() {
339 let s = "line1\r\nline2";
340 let lit = go_string_literal(s);
341 assert!(
342 !lit.as_bytes().contains(&b'\r'),
343 "go_string_literal emitted a literal CR — gofmt would reject this: {lit:?}"
344 );
345 assert!(
346 lit.starts_with('"'),
347 "expected double-quoted string for CR input, got: {lit:?}"
348 );
349 }
350
351 #[test]
354 fn go_string_literal_plain_string_uses_backtick() {
355 let s = "Hello World\nwith newline";
356 let lit = go_string_literal(s);
357 assert!(
358 lit.starts_with('`'),
359 "expected backtick form for plain string, got: {lit:?}"
360 );
361 }
362
363 #[test]
365 fn go_string_literal_backtick_in_string_uses_quoted_form() {
366 let s = "has `backtick`";
367 let lit = go_string_literal(s);
368 assert!(
369 lit.starts_with('"'),
370 "expected double-quoted form when string contains backtick, got: {lit:?}"
371 );
372 }
373}