sub_fixer/
lib.rs

1#![feature(string_remove_matches, iter_intersperse)]
2
3#[macro_use]
4extern crate lazy_static;
5
6mod regex {
7    use regex::Regex;
8
9    const STR_TIMESTAMP_PATTERN: &str =
10        r"^(\d{2}):(\d{2}):(\d{2}),(\d{3})\s-->\s(\d{2}):(\d{2}):(\d{2}),(\d{3})$";
11    const STR_LINE_NUM_PATTERN: &str = r"^\d+$";
12
13    lazy_static! {
14        pub static ref SRT_TIMESTAMP_REGEX: Regex = Regex::new(STR_TIMESTAMP_PATTERN).unwrap();
15        pub static ref SRT_LINE_NUM_REGEX: Regex = Regex::new(STR_LINE_NUM_PATTERN).unwrap();
16    }
17
18    pub const PERSIAN_NUMBERS: [(char, &str); 10] = [
19        ('0', "۰"),
20        ('1', "۱"),
21        ('2', "۲"),
22        ('3', "۳"),
23        ('4', "۴"),
24        ('5', "۵"),
25        ('6', "۶"),
26        ('7', "۷"),
27        ('8', "۸"),
28        ('9', "۹"),
29    ];
30}
31
32pub struct Subtitle {
33    contents: String,
34}
35
36impl Subtitle {
37    pub fn new(contents: String) -> Self {
38        Self { contents }
39    }
40
41    pub fn fix(mut self) -> String {
42        self.remove_italics();
43        self.replace_arabic_chars();
44        self.replace_question_mark();
45        self.remove_rtl_char();
46        self.fix_others();
47        self.contents
48    }
49
50    fn remove_italics(&mut self) {
51        self.contents.remove_matches("<i>");
52        self.contents.remove_matches("</i>");
53    }
54
55    fn replace_arabic_chars(&mut self) {
56        self.contents = self.contents.replace('ي', "ی");
57        self.contents = self.contents.replace('ك', "ک");
58    }
59
60    fn replace_question_mark(&mut self) {
61        self.contents = self.contents.replace('?', "؟");
62    }
63
64    fn remove_rtl_char(&mut self) {
65        self.contents.remove_matches('\u{202b}');
66    }
67
68    fn fix_others(&mut self) {
69        let fix_line = |line: &str| {
70            match line {
71                l if regex::SRT_TIMESTAMP_REGEX.is_match(l) => l.into(),
72                l if l.trim().is_empty() => l.into(),
73                l if regex::SRT_LINE_NUM_REGEX.is_match(l) => l.into(),
74
75                // This is subtitle
76                l => {
77                    //TODO: remove [\.!?]* groups in line
78
79                    // Replace digits with Persian digits
80                    let mut l = l.to_string();
81                    for (n, p) in regex::PERSIAN_NUMBERS.into_iter() {
82                        l = l.replace(n, p);
83                    }
84
85                    // TODO: Make this optional
86                    // Put RTL char in start of line (it forces some player to show that line RTL)
87                    // format!("\u{202b}{l}")
88                    l
89                }
90            }
91        };
92
93        let contents = self
94            .contents
95            .lines()
96            .map(fix_line)
97            .intersperse("\n".to_owned())
98            .collect::<String>();
99        self.contents = contents;
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use crate::Subtitle;
106
107    #[test]
108    fn will_remove_italics() {
109        let input = r#"1
11000:08:26,025 --> 00:08:30,091
111<i>!زود باشین برین</i>
112
113!نمی‌ذارم هیچ کدومتون اینجا بمیرین
114
1152
11600:08:22,037 --> 00:08:24,008
117!همه عجله کنین"#;
118
119        let expected = r#"1
12000:08:26,025 --> 00:08:30,091
121!زود باشین برین
122
123!نمی‌ذارم هیچ کدومتون اینجا بمیرین
124
1252
12600:08:22,037 --> 00:08:24,008
127!همه عجله کنین"#;
128
129        let output = Subtitle::new(input.to_string()).fix();
130
131        assert_eq!(expected, output)
132    }
133
134    #[test]
135    fn will_replace_arabic_chars() {
136        let input = r#"1
13700:08:26,025 --> 00:08:30,091
138!زود باشین برين
139
140!نمی‌ذارم هیچ كدومتون اینجا بمیرین
141
1422
14300:08:22,037 --> 00:08:24,008
144!همه عجله کنین"#;
145
146        let expected = r#"1
14700:08:26,025 --> 00:08:30,091
148!زود باشین برین
149
150!نمی‌ذارم هیچ کدومتون اینجا بمیرین
151
1522
15300:08:22,037 --> 00:08:24,008
154!همه عجله کنین"#;
155
156        let output = Subtitle::new(input.to_string()).fix();
157
158        assert_eq!(expected, output)
159    }
160
161    #[test]
162    fn will_replace_question_mark() {
163        let input = r#"1
16400:08:26,025 --> 00:08:30,091
165؟زود باشین برين
166
167?نمی‌ذارم هیچ كدومتون اینجا بمیرین
168
1692
17000:08:22,037 --> 00:08:24,008
171!همه عجله کنین"#;
172
173        let expected = r#"1
17400:08:26,025 --> 00:08:30,091
175؟زود باشین برین
176
177؟نمی‌ذارم هیچ کدومتون اینجا بمیرین
178
1792
18000:08:22,037 --> 00:08:24,008
181!همه عجله کنین"#;
182
183        let output = Subtitle::new(input.to_string()).fix();
184
185        assert_eq!(expected, output)
186    }
187
188    #[test]
189    fn will_replace_numbers_with_persian_numbers() {
190        let input = r#"1
19100:08:26,025 --> 00:08:30,091
192؟زود باشین برين 12
193
194؟نمی‌ذارم 2 هیچ كدومتون 0912 اینجا بمیرین
195
1962
19700:08:22,037 --> 00:08:24,008
198!همه 34عجله کنین"#;
199
200        let expected = r#"1
20100:08:26,025 --> 00:08:30,091
202؟زود باشین برین ۱۲
203
204؟نمی‌ذارم ۲ هیچ کدومتون ۰۹۱۲ اینجا بمیرین
205
2062
20700:08:22,037 --> 00:08:24,008
208!همه ۳۴عجله کنین"#;
209
210        let output = Subtitle::new(input.to_string()).fix();
211
212        assert_eq!(expected, output)
213    }
214}