subx_cli/core/formats/
styling.rs1use regex::Regex;
14
15use crate::core::formats::StylingInfo;
16use crate::core::formats::converter::FormatConverter;
17
18impl FormatConverter {
19 pub(crate) fn extract_srt_styling(&self, text: &str) -> crate::Result<StylingInfo> {
21 let mut styling = StylingInfo::default();
22 if text.contains("<b>") || text.contains("<B>") {
23 styling.bold = true;
24 }
25 if text.contains("<i>") || text.contains("<I>") {
26 styling.italic = true;
27 }
28 if text.contains("<u>") || text.contains("<U>") {
29 styling.underline = true;
30 }
31 if let Some(color) = self.extract_color_from_tags(text) {
32 styling.color = Some(color);
33 }
34 Ok(styling)
35 }
36
37 pub(crate) fn convert_srt_tags_to_ass(&self, text: &str) -> String {
39 let mut result = text.to_string();
40 result = result.replace("<b>", "{\\b1}").replace("</b>", "{\\b0}");
41 result = result.replace("<i>", "{\\i1}").replace("</i>", "{\\i0}");
42 result = result.replace("<u>", "{\\u1}").replace("</u>", "{\\u0}");
43 let color_regex = Regex::new(r#"<font color=\"([^\"]+)\">"#).unwrap();
44 result = color_regex
45 .replace_all(&result, |caps: ®ex::Captures| {
46 let color = &caps[1];
47 format!("{{\\c&H{}&}}", self.convert_color_to_ass(color))
48 })
49 .to_string();
50 result = result.replace("</font>", "{\\c}");
51 result
52 }
53
54 pub(crate) fn strip_ass_tags(&self, text: &str) -> String {
56 let tag_regex = Regex::new(r"\{[^}]*\}").unwrap();
57 tag_regex.replace_all(text, "").to_string()
58 }
59
60 pub(crate) fn convert_ass_tags_to_srt(&self, text: &str) -> String {
62 let mut result = text.to_string();
63 let bold_regex = Regex::new(r"\{\\b1\}([^\{]*)\{\\b0\}").unwrap();
64 result = bold_regex.replace_all(&result, "<b>$1</b>").to_string();
65 let italic_regex = Regex::new(r"\{\\i1\}([^\{]*)\{\\i0\}").unwrap();
66 result = italic_regex.replace_all(&result, "<i>$1</i>").to_string();
67 let underline_regex = Regex::new(r"\{\\u1\}([^\{]*)\{\\u0\}").unwrap();
68 result = underline_regex
69 .replace_all(&result, "<u>$1</u>")
70 .to_string();
71 result
72 }
73
74 pub(crate) fn extract_color_from_tags(&self, _text: &str) -> Option<String> {
76 None
77 }
78
79 pub(crate) fn convert_color_to_ass(&self, color: &str) -> String {
81 color.trim_start_matches('#').to_string()
82 }
83
84 pub(crate) fn convert_srt_tags_to_vtt(&self, text: &str) -> String {
86 text.to_string()
87 }
88 pub(crate) fn convert_vtt_tags_to_srt(&self, text: &str) -> String {
90 text.to_string()
92 }
93 pub(crate) fn strip_vtt_tags(&self, text: &str) -> String {
95 let tag_regex = Regex::new(r"</?[^>]+>").unwrap();
96 tag_regex.replace_all(text, "").to_string()
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use crate::core::formats::converter::{ConversionConfig, FormatConverter};
103
104 fn make_converter() -> FormatConverter {
105 FormatConverter::new(ConversionConfig {
106 preserve_styling: true,
107 target_encoding: "UTF-8".to_string(),
108 keep_original: false,
109 validate_output: false,
110 })
111 }
112
113 #[test]
116 fn test_extract_srt_styling_empty() {
117 let c = make_converter();
118 let s = c.extract_srt_styling("").unwrap();
119 assert!(!s.bold);
120 assert!(!s.italic);
121 assert!(!s.underline);
122 assert!(s.color.is_none());
123 }
124
125 #[test]
126 fn test_extract_srt_styling_bold_lowercase() {
127 let c = make_converter();
128 let s = c.extract_srt_styling("<b>Hello</b>").unwrap();
129 assert!(s.bold);
130 assert!(!s.italic);
131 assert!(!s.underline);
132 }
133
134 #[test]
135 fn test_extract_srt_styling_bold_uppercase() {
136 let c = make_converter();
137 let s = c.extract_srt_styling("<B>Hello</B>").unwrap();
138 assert!(s.bold);
139 }
140
141 #[test]
142 fn test_extract_srt_styling_italic_lowercase() {
143 let c = make_converter();
144 let s = c.extract_srt_styling("<i>Hello</i>").unwrap();
145 assert!(s.italic);
146 assert!(!s.bold);
147 assert!(!s.underline);
148 }
149
150 #[test]
151 fn test_extract_srt_styling_italic_uppercase() {
152 let c = make_converter();
153 let s = c.extract_srt_styling("<I>text</I>").unwrap();
154 assert!(s.italic);
155 }
156
157 #[test]
158 fn test_extract_srt_styling_underline_lowercase() {
159 let c = make_converter();
160 let s = c.extract_srt_styling("<u>text</u>").unwrap();
161 assert!(s.underline);
162 assert!(!s.bold);
163 assert!(!s.italic);
164 }
165
166 #[test]
167 fn test_extract_srt_styling_underline_uppercase() {
168 let c = make_converter();
169 let s = c.extract_srt_styling("<U>text</U>").unwrap();
170 assert!(s.underline);
171 }
172
173 #[test]
174 fn test_extract_srt_styling_all_tags() {
175 let c = make_converter();
176 let s = c
177 .extract_srt_styling("<b><i><u>styled</u></i></b>")
178 .unwrap();
179 assert!(s.bold);
180 assert!(s.italic);
181 assert!(s.underline);
182 }
183
184 #[test]
185 fn test_extract_srt_styling_mixed_case_independent() {
186 let c = make_converter();
187 let s = c.extract_srt_styling("<B>bold</B> <i>italic</i>").unwrap();
188 assert!(s.bold);
189 assert!(s.italic);
190 assert!(!s.underline);
191 }
192
193 #[test]
194 fn test_extract_srt_styling_no_tags() {
195 let c = make_converter();
196 let s = c.extract_srt_styling("plain text").unwrap();
197 assert!(!s.bold);
198 assert!(!s.italic);
199 assert!(!s.underline);
200 assert!(s.color.is_none());
201 }
202
203 #[test]
206 fn test_extract_color_from_tags_always_none() {
207 let c = make_converter();
208 assert!(
209 c.extract_color_from_tags(r##"<font color="#FF0000">text</font>"##)
210 .is_none()
211 );
212 assert!(c.extract_color_from_tags("").is_none());
213 assert!(c.extract_color_from_tags("anything").is_none());
214 }
215
216 #[test]
219 fn test_convert_color_to_ass_strips_hash() {
220 let c = make_converter();
221 assert_eq!(c.convert_color_to_ass("#FF0000"), "FF0000");
222 }
223
224 #[test]
225 fn test_convert_color_to_ass_no_hash() {
226 let c = make_converter();
227 assert_eq!(c.convert_color_to_ass("FF0000"), "FF0000");
228 }
229
230 #[test]
231 fn test_convert_color_to_ass_empty() {
232 let c = make_converter();
233 assert_eq!(c.convert_color_to_ass(""), "");
234 }
235
236 #[test]
237 fn test_convert_color_to_ass_named_color() {
238 let c = make_converter();
239 assert_eq!(c.convert_color_to_ass("red"), "red");
240 }
241
242 #[test]
245 fn test_convert_srt_to_ass_bold() {
246 let c = make_converter();
247 assert_eq!(
248 c.convert_srt_tags_to_ass("<b>Hello</b>"),
249 "{\\b1}Hello{\\b0}"
250 );
251 }
252
253 #[test]
254 fn test_convert_srt_to_ass_italic() {
255 let c = make_converter();
256 assert_eq!(
257 c.convert_srt_tags_to_ass("<i>Hello</i>"),
258 "{\\i1}Hello{\\i0}"
259 );
260 }
261
262 #[test]
263 fn test_convert_srt_to_ass_underline() {
264 let c = make_converter();
265 assert_eq!(
266 c.convert_srt_tags_to_ass("<u>Hello</u>"),
267 "{\\u1}Hello{\\u0}"
268 );
269 }
270
271 #[test]
272 fn test_convert_srt_to_ass_font_color_with_hash() {
273 let c = make_converter();
274 let result = c.convert_srt_tags_to_ass(r##"<font color="#FF0000">red</font>"##);
275 assert!(result.contains("FF0000"), "got: {result}");
276 assert!(result.contains("{\\c}"), "got: {result}");
277 }
278
279 #[test]
280 fn test_convert_srt_to_ass_font_color_without_hash() {
281 let c = make_converter();
282 let result = c.convert_srt_tags_to_ass(r##"<font color="AABBCC">text</font>"##);
283 assert!(result.contains("AABBCC"), "got: {result}");
284 }
285
286 #[test]
287 fn test_convert_srt_to_ass_empty() {
288 let c = make_converter();
289 assert_eq!(c.convert_srt_tags_to_ass(""), "");
290 }
291
292 #[test]
293 fn test_convert_srt_to_ass_no_tags() {
294 let c = make_converter();
295 assert_eq!(c.convert_srt_tags_to_ass("plain"), "plain");
296 }
297
298 #[test]
299 fn test_convert_srt_to_ass_combined() {
300 let c = make_converter();
301 let result = c.convert_srt_tags_to_ass("<b><i>bold italic</i></b>");
302 assert!(result.contains("{\\b1}"), "got: {result}");
303 assert!(result.contains("{\\b0}"), "got: {result}");
304 assert!(result.contains("{\\i1}"), "got: {result}");
305 assert!(result.contains("{\\i0}"), "got: {result}");
306 }
307
308 #[test]
309 fn test_convert_srt_to_ass_font_close_tag() {
310 let c = make_converter();
311 let result = c.convert_srt_tags_to_ass("</font>");
312 assert_eq!(result, "{\\c}");
313 }
314
315 #[test]
318 fn test_strip_ass_tags_basic() {
319 let c = make_converter();
320 assert_eq!(c.strip_ass_tags("{\\b1}Hello{\\b0}"), "Hello");
321 }
322
323 #[test]
324 fn test_strip_ass_tags_multiple() {
325 let c = make_converter();
326 assert_eq!(
327 c.strip_ass_tags("{\\i1}italic{\\i0} and {\\b1}bold{\\b0}"),
328 "italic and bold"
329 );
330 }
331
332 #[test]
333 fn test_strip_ass_tags_empty() {
334 let c = make_converter();
335 assert_eq!(c.strip_ass_tags(""), "");
336 }
337
338 #[test]
339 fn test_strip_ass_tags_no_tags() {
340 let c = make_converter();
341 assert_eq!(c.strip_ass_tags("plain text"), "plain text");
342 }
343
344 #[test]
345 fn test_strip_ass_tags_only_tag() {
346 let c = make_converter();
347 assert_eq!(c.strip_ass_tags("{\\pos(320,240)}"), "");
348 }
349
350 #[test]
351 fn test_strip_ass_tags_color_tag() {
352 let c = make_converter();
353 let result = c.strip_ass_tags("{\\c&HFF0000&}colored text{\\c}");
354 assert_eq!(result, "colored text");
355 }
356
357 #[test]
360 fn test_convert_ass_to_srt_bold() {
361 let c = make_converter();
362 assert_eq!(
363 c.convert_ass_tags_to_srt("{\\b1}Hello{\\b0}"),
364 "<b>Hello</b>"
365 );
366 }
367
368 #[test]
369 fn test_convert_ass_to_srt_italic() {
370 let c = make_converter();
371 assert_eq!(
372 c.convert_ass_tags_to_srt("{\\i1}Hello{\\i0}"),
373 "<i>Hello</i>"
374 );
375 }
376
377 #[test]
378 fn test_convert_ass_to_srt_underline() {
379 let c = make_converter();
380 assert_eq!(
381 c.convert_ass_tags_to_srt("{\\u1}Hello{\\u0}"),
382 "<u>Hello</u>"
383 );
384 }
385
386 #[test]
387 fn test_convert_ass_to_srt_empty() {
388 let c = make_converter();
389 assert_eq!(c.convert_ass_tags_to_srt(""), "");
390 }
391
392 #[test]
393 fn test_convert_ass_to_srt_no_tags() {
394 let c = make_converter();
395 assert_eq!(c.convert_ass_tags_to_srt("plain"), "plain");
396 }
397
398 #[test]
399 fn test_convert_ass_to_srt_multiple() {
400 let c = make_converter();
401 let result = c.convert_ass_tags_to_srt("{\\b1}bold{\\b0} and {\\i1}italic{\\i0}");
402 assert_eq!(result, "<b>bold</b> and <i>italic</i>");
403 }
404
405 #[test]
408 fn test_convert_srt_to_vtt_passthrough() {
409 let c = make_converter();
410 assert_eq!(c.convert_srt_tags_to_vtt("Hello"), "Hello");
411 assert_eq!(c.convert_srt_tags_to_vtt("<b>bold</b>"), "<b>bold</b>");
412 assert_eq!(c.convert_srt_tags_to_vtt(""), "");
413 }
414
415 #[test]
418 fn test_convert_vtt_to_srt_passthrough() {
419 let c = make_converter();
420 assert_eq!(c.convert_vtt_tags_to_srt("Hello"), "Hello");
421 assert_eq!(c.convert_vtt_tags_to_srt("<b>bold</b>"), "<b>bold</b>");
422 assert_eq!(c.convert_vtt_tags_to_srt(""), "");
423 }
424
425 #[test]
428 fn test_strip_vtt_tags_basic() {
429 let c = make_converter();
430 assert_eq!(c.strip_vtt_tags("<b>Hello</b>"), "Hello");
431 }
432
433 #[test]
434 fn test_strip_vtt_tags_multiple() {
435 let c = make_converter();
436 assert_eq!(
437 c.strip_vtt_tags("<i>italic</i> and <b>bold</b>"),
438 "italic and bold"
439 );
440 }
441
442 #[test]
443 fn test_strip_vtt_tags_empty() {
444 let c = make_converter();
445 assert_eq!(c.strip_vtt_tags(""), "");
446 }
447
448 #[test]
449 fn test_strip_vtt_tags_no_tags() {
450 let c = make_converter();
451 assert_eq!(c.strip_vtt_tags("plain text"), "plain text");
452 }
453
454 #[test]
455 fn test_strip_vtt_tags_nested() {
456 let c = make_converter();
457 assert_eq!(c.strip_vtt_tags("<b><i>nested</i></b>"), "nested");
458 }
459
460 #[test]
461 fn test_strip_vtt_tags_with_attributes() {
462 let c = make_converter();
463 assert_eq!(
464 c.strip_vtt_tags(r##"<font color="red">text</font>"##),
465 "text"
466 );
467 }
468
469 #[test]
470 fn test_strip_vtt_tags_self_closing_like() {
471 let c = make_converter();
472 assert_eq!(c.strip_vtt_tags("<br>text"), "text");
473 }
474
475 #[test]
478 fn test_roundtrip_srt_to_ass_strip() {
479 let c = make_converter();
480 let srt = "<b>Hello World</b>";
481 let ass = c.convert_srt_tags_to_ass(srt);
482 let stripped = c.strip_ass_tags(&ass);
483 assert_eq!(stripped, "Hello World");
484 }
485
486 #[test]
487 fn test_roundtrip_srt_to_ass_to_srt() {
488 let c = make_converter();
489 let original = "<b>text</b>";
490 let ass = c.convert_srt_tags_to_ass(original);
491 let back = c.convert_ass_tags_to_srt(&ass);
492 assert_eq!(back, original);
493 }
494
495 #[test]
496 fn test_roundtrip_italic_srt_ass_srt() {
497 let c = make_converter();
498 let original = "<i>text</i>";
499 let ass = c.convert_srt_tags_to_ass(original);
500 let back = c.convert_ass_tags_to_srt(&ass);
501 assert_eq!(back, original);
502 }
503
504 #[test]
505 fn test_roundtrip_underline_srt_ass_srt() {
506 let c = make_converter();
507 let original = "<u>text</u>";
508 let ass = c.convert_srt_tags_to_ass(original);
509 let back = c.convert_ass_tags_to_srt(&ass);
510 assert_eq!(back, original);
511 }
512}