use crate::extract::read_unicode_escape;
pub fn sanitize_for_xgettext(src: &str) -> String {
let chars: Vec<char> = src.chars().collect();
let n = chars.len();
let mut out = String::with_capacity(src.len());
let mut i = 0;
while i < n {
let c = chars[i];
if c == '/' && i + 1 < n && chars[i + 1] == '/' {
while i < n && chars[i] != '\n' {
out.push(chars[i]);
i += 1;
}
} else if c == '/' && i + 1 < n && chars[i + 1] == '*' {
out.push('/');
out.push('*');
i += 2;
let mut depth = 1usize;
while i < n && depth > 0 {
if i + 1 < n && chars[i] == '/' && chars[i + 1] == '*' {
out.push('/');
out.push('*');
i += 2;
depth += 1;
} else if i + 1 < n && chars[i] == '*' && chars[i + 1] == '/' {
out.push('*');
out.push('/');
i += 2;
depth -= 1;
} else {
out.push(chars[i]);
i += 1;
}
}
} else if c == '"' {
out.push(c);
i += 1;
while i < n {
let ch = chars[i];
if ch == '\\'
&& chars.get(i + 1) == Some(&'u')
&& let Some((decoded, next)) = read_unicode_escape(&chars, i + 1)
{
match decoded {
'"' | '\\' => out.extend(['\\', decoded]),
'\n' => out.push_str("\\n"),
'\t' => out.push_str("\\t"),
_ => out.push(decoded),
}
i = next;
continue;
}
out.push(ch);
if ch == '\\' && i + 1 < n {
out.push(chars[i + 1]);
i += 2;
continue;
}
i += 1;
if ch == '"' {
break;
}
}
} else if c == 'r' {
if let Some(hashes) = raw_string_hash_count(&chars, i) {
for _ in 0..(hashes + 2) {
out.push(' ');
}
i += hashes + 2;
blank_raw_string_body(&chars, &mut i, hashes, &mut out);
} else {
out.push(c);
i += 1;
}
} else if c == '\'' {
if let Some(end) = char_literal_end(&chars, i) {
out.extend(&chars[i..=end]);
i = end + 1;
} else {
out.push(' ');
i += 1;
}
} else {
out.push(c);
i += 1;
}
}
out
}
const fn raw_string_hash_count(chars: &[char], i: usize) -> Option<usize> {
let n = chars.len();
let mut j = i + 1;
let mut hashes = 0usize;
while j < n && chars[j] == '#' {
hashes += 1;
j += 1;
}
if j < n && chars[j] == '"' { Some(hashes) } else { None }
}
fn blank_raw_string_body(chars: &[char], i: &mut usize, hashes: usize, out: &mut String) {
let n = chars.len();
while *i < n {
if chars[*i] == '"' {
let mut k = *i + 1;
let mut matched = 0usize;
while k < n && matched < hashes && chars[k] == '#' {
matched += 1;
k += 1;
}
if matched == hashes {
for _ in 0..=hashes {
out.push(' ');
}
*i = k;
return;
}
}
if chars[*i] == '\n' {
out.push('\n');
} else {
out.push(' ');
}
*i += 1;
}
}
const fn char_literal_end(chars: &[char], i: usize) -> Option<usize> {
let n = chars.len();
let mut j = i + 1;
if j >= n {
return None;
}
if chars[j] == '\\' {
j += 1;
if j >= n {
return None;
}
if chars[j] == 'u' && j + 1 < n && chars[j + 1] == '{' {
let mut k = j + 2;
while k < n && chars[k] != '}' {
k += 1;
}
if k >= n {
return None;
}
j = k + 1;
} else {
j += 1;
}
} else {
j += 1;
}
if j < n && chars[j] == '\'' { Some(j) } else { None }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn preserves_normal_strings_and_comments() {
let src = "// TRANSLATORS: hi\nlet x = t(\"hello 'world'\");\n";
assert_eq!(sanitize_for_xgettext(src), src);
}
#[test]
fn neutralizes_lifetimes_without_changing_length_or_lines() {
let src = "fn f<'a>(s: &'a str) -> &'a str { s }\n";
let out = sanitize_for_xgettext(src);
assert_eq!(out.chars().count(), src.chars().count());
assert_eq!(out.lines().count(), src.lines().count());
assert!(!out.contains('\''));
}
#[test]
fn preserves_char_literals() {
let src = "let c = '{'; let d = '\\n'; let e = '\\'';\n";
let out = sanitize_for_xgettext(src);
assert!(out.contains("'{'"));
assert!(out.contains("'\\n'"));
assert!(out.contains("'\\''"));
}
#[test]
fn decodes_unicode_escapes_in_strings() {
let src = "t(\"{} on \\u{201c}{}\\u{201d}: \\u{22}\\u{5c}\");\nlet c = '\\u{2603}';\n";
assert_eq!(sanitize_for_xgettext(src), "t(\"{} on \u{201c}{}\u{201d}: \\\"\\\\\");\nlet c = '\\u{2603}';\n");
}
#[test]
fn blanks_raw_strings_but_keeps_line_count() {
let src = "let xml = r#\"\n<a href=\"x\">it's</a>\n\"#;\nlet y = t(\"real\");\n";
let out = sanitize_for_xgettext(src);
assert_eq!(out.lines().count(), src.lines().count());
assert!(out.contains("t(\"real\")"));
assert!(!out.contains("href"));
}
}