pub fn clean(text: &str) -> String {
const STRIP: &[char] = &[
'\u{feff}', '\u{200b}', '\u{200c}', '\u{200d}', '\u{2060}', '\u{a0}', ' ', '\t',
];
text.replace("\r\n", "\n")
.replace('\r', "\n")
.split('\n')
.map(|l| l.trim_matches(STRIP).trim())
.filter(|l| !l.is_empty() && !is_bare_timestamp(l))
.collect::<Vec<_>>()
.join("\n")
}
fn is_bare_timestamp(line: &str) -> bool {
let Some(inner) = line.strip_prefix('[').and_then(|l| l.strip_suffix(']')) else {
return false;
};
let (main, frac) = match inner.split_once('.') {
Some((m, f)) => (m, Some(f)),
None => (inner, None),
};
if let Some(f) = frac
&& (!(1..=3).contains(&f.len()) || !f.bytes().all(|b| b.is_ascii_digit()))
{
return false;
}
let parts: Vec<&str> = main.split(':').collect();
(2..=3).contains(&parts.len())
&& parts
.iter()
.all(|p| p.len() == 2 && p.bytes().all(|b| b.is_ascii_digit()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn clean_rules() {
let raw = "\u{feff}[ti:夜曲]\r\n\r\n [00:04.99]词:方文山 \r[00:10.00]\n[01:02:03.5]\n\t\n[00:12.81]一闪一闪";
assert_eq!(
clean(raw),
"[ti:夜曲]\n[00:04.99]词:方文山\n[00:12.81]一闪一闪"
);
}
#[test]
fn bare_timestamps() {
assert!(is_bare_timestamp("[00:10]"));
assert!(is_bare_timestamp("[00:10.1]"));
assert!(is_bare_timestamp("[00:10.123]"));
assert!(is_bare_timestamp("[01:00:10.12]"));
assert!(!is_bare_timestamp("[00:10.1234]"));
assert!(!is_bare_timestamp("[ti:x]"));
assert!(!is_bare_timestamp("[00:10]歌词"));
assert!(!is_bare_timestamp("[0:10]"));
}
}