pub fn escape_python(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => {
out.push_str(&format!("\\x{:02x}", c as u32));
}
c => out.push(c),
}
}
out
}
pub fn escape_rust(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}
pub fn raw_string_hashes(s: &str) -> usize {
let mut max_hashes = 0;
let mut current = 0;
let mut after_quote = false;
for ch in s.chars() {
if ch == '"' {
after_quote = true;
current = 0;
} else if ch == '#' && after_quote {
current += 1;
max_hashes = max_hashes.max(current);
} else {
after_quote = false;
current = 0;
}
}
max_hashes + 1
}
pub fn rust_raw_string(s: &str) -> String {
let hashes = raw_string_hashes(s);
let h: String = "#".repeat(hashes);
format!("r{h}\"{s}\"{h}")
}
pub fn escape_js(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}
pub fn escape_js_template(s: &str) -> String {
s.replace('\\', "\\\\").replace('`', "\\`").replace('$', "\\$")
}
fn go_needs_quoted(s: &str) -> bool {
s.contains('`') || s.bytes().any(|b| b == 0 || b == b'\r')
}
pub fn go_string_literal(s: &str) -> String {
if go_needs_quoted(s) {
format!("\"{}\"", escape_go(s))
} else {
format!("`{s}`")
}
}
pub fn escape_go(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for b in s.bytes() {
match b {
b'\\' => out.push_str("\\\\"),
b'"' => out.push_str("\\\""),
b'\n' => out.push_str("\\n"),
b'\r' => out.push_str("\\r"),
b'\t' => out.push_str("\\t"),
0 => out.push_str("\\x00"),
b if b < 0x20 || b == 0x7f => {
out.push_str(&format!("\\x{b:02x}"));
}
_ => out.push(b as char),
}
}
out
}
pub fn escape_java(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}
pub fn escape_kotlin(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('$', "\\$")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}
pub fn escape_csharp(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}
pub fn escape_php(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('$', "\\$")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}
pub fn escape_ruby(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('#', "\\#")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}
pub fn escape_ruby_single(s: &str) -> String {
s.replace('\\', "\\\\").replace('\'', "\\'")
}
pub fn ruby_template_to_interpolation(template: &str) -> String {
let mut out = String::with_capacity(template.len() + 8);
let mut chars = template.chars().peekable();
while let Some(ch) = chars.next() {
match ch {
'{' => {
let is_ident_start = chars.peek().is_some_and(|&c| c.is_ascii_alphabetic() || c == '_');
if is_ident_start {
let mut ident = String::new();
while let Some(&c) = chars.peek() {
if c.is_ascii_alphanumeric() || c == '_' {
ident.push(chars.next().unwrap());
} else {
break;
}
}
if chars.peek() == Some(&'}') {
chars.next(); out.push('#');
out.push('{');
out.push_str(&ident);
out.push('}');
} else {
out.push('{');
out.push_str(&ident);
}
} else {
out.push('{');
}
}
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'#' => out.push_str("\\#"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c => out.push(c),
}
}
out
}
pub fn r_template_to_paste0(template: &str) -> String {
enum Seg {
Lit(String),
Param(String),
}
let mut segments: Vec<Seg> = Vec::new();
let mut lit = String::new();
let mut chars = template.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '{' {
let is_ident_start = chars.peek().is_some_and(|&c| c.is_ascii_alphabetic() || c == '_');
if is_ident_start {
let mut ident = String::new();
while let Some(&c) = chars.peek() {
if c.is_ascii_alphanumeric() || c == '_' {
ident.push(chars.next().unwrap());
} else {
break;
}
}
if chars.peek() == Some(&'}') {
chars.next();
if !lit.is_empty() {
segments.push(Seg::Lit(lit.clone()));
lit.clear();
}
segments.push(Seg::Param(ident));
continue;
}
lit.push('{');
lit.push_str(&ident);
} else {
lit.push('{');
}
} else {
lit.push(ch);
}
}
if !lit.is_empty() {
segments.push(Seg::Lit(lit));
}
match segments.as_slice() {
[] => r#""""#.to_string(),
[Seg::Param(p)] => p.clone(),
segs => {
let args: Vec<String> = segs
.iter()
.map(|s| match s {
Seg::Lit(l) => format!("\"{}\"", escape_r(l)),
Seg::Param(p) => p.clone(),
})
.collect();
format!("paste0({})", args.join(", "))
}
}
}
pub fn ruby_needs_double_quotes(s: &str) -> bool {
s.contains('\n') || s.contains('\r') || s.contains('\t') || s.contains('\0')
}
pub fn ruby_string_literal(s: &str) -> String {
if ruby_needs_double_quotes(s) {
format!("\"{}\"", escape_ruby(s))
} else {
format!("'{}'", escape_ruby_single(s))
}
}
pub fn escape_elixir(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('#', "\\#")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}
pub fn escape_r(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}
pub fn escape_c(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}
pub fn sanitize_ident(s: &str) -> String {
let mut result = String::with_capacity(s.len());
for ch in s.chars() {
if ch.is_ascii_alphanumeric() || ch == '_' {
result.push(ch);
} else {
result.push('_');
}
}
let trimmed = result.trim_start_matches(|c: char| c.is_ascii_digit());
if trimmed.is_empty() {
"_".to_string()
} else {
trimmed.to_string()
}
}
pub fn sanitize_filename(s: &str) -> String {
s.chars()
.map(|c| if c.is_ascii_alphanumeric() || c == '_' { c } else { '_' })
.collect::<String>()
.to_lowercase()
}
pub fn expand_fixture_templates(s: &str) -> String {
const PREFIX: &str = "{{ repeat '";
const SUFFIX: &str = " times }}";
let mut result = String::with_capacity(s.len());
let mut remaining = s;
while let Some(start) = remaining.find(PREFIX) {
result.push_str(&remaining[..start]);
let after_prefix = &remaining[start + PREFIX.len()..];
if let Some(quote_pos) = after_prefix.find("' ") {
let ch = &after_prefix[..quote_pos];
let after_quote = &after_prefix[quote_pos + 2..];
if let Some(end) = after_quote.find(SUFFIX) {
let count_str = after_quote[..end].trim();
if let Ok(count) = count_str.parse::<usize>() {
result.push_str(&ch.repeat(count));
remaining = &after_quote[end + SUFFIX.len()..];
continue;
}
}
}
result.push_str(PREFIX);
remaining = after_prefix;
}
result.push_str(remaining);
result
}
pub fn escape_shell(s: &str) -> String {
s.replace('\'', r"'\''")
}
pub fn escape_gleam(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}
pub fn escape_zig(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn go_string_literal_nul_bytes_use_quoted_form() {
let s = "Hello\x00World";
let lit = go_string_literal(s);
assert!(
!lit.as_bytes().contains(&0u8),
"go_string_literal emitted a NUL byte — gofmt would reject this: {lit:?}"
);
assert!(
lit.starts_with('"'),
"expected double-quoted string for NUL input, got: {lit:?}"
);
assert!(
lit.contains("\\x00"),
"expected \\x00 escape sequence for NUL byte, got: {lit:?}"
);
}
#[test]
fn go_string_literal_carriage_return_uses_quoted_form() {
let s = "line1\r\nline2";
let lit = go_string_literal(s);
assert!(
!lit.as_bytes().contains(&b'\r'),
"go_string_literal emitted a literal CR — gofmt would reject this: {lit:?}"
);
assert!(
lit.starts_with('"'),
"expected double-quoted string for CR input, got: {lit:?}"
);
}
#[test]
fn go_string_literal_plain_string_uses_backtick() {
let s = "Hello World\nwith newline";
let lit = go_string_literal(s);
assert!(
lit.starts_with('`'),
"expected backtick form for plain string, got: {lit:?}"
);
}
#[test]
fn go_string_literal_backtick_in_string_uses_quoted_form() {
let s = "has `backtick`";
let lit = go_string_literal(s);
assert!(
lit.starts_with('"'),
"expected double-quoted form when string contains backtick, got: {lit:?}"
);
}
}