1use crate::core::errors::EditorError;
7use crate::core::{EditorDocument, Result};
8use ass_core::parser::ast::EventType;
9
10#[cfg(not(feature = "std"))]
11use alloc::{
12 format,
13 string::{String, ToString},
14 vec::Vec,
15};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum SubtitleFormat {
20 ASS,
22 SSA,
24 SRT,
26 WebVTT,
28 PlainText,
30}
31
32impl SubtitleFormat {
33 pub fn from_extension(ext: &str) -> Option<Self> {
35 match ext.to_lowercase().as_str() {
36 "ass" => Some(Self::ASS),
37 "ssa" => Some(Self::SSA),
38 "srt" => Some(Self::SRT),
39 "vtt" | "webvtt" => Some(Self::WebVTT),
40 "txt" => Some(Self::PlainText),
41 _ => None,
42 }
43 }
44
45 pub fn from_content(content: &str) -> Self {
47 if content.contains("[Script Info]") || content.contains("[Events]") {
48 Self::ASS
49 } else if content.starts_with("WEBVTT") {
50 Self::WebVTT
51 } else if content.contains("-->") && !content.starts_with("WEBVTT") {
52 Self::SRT
53 } else {
54 Self::PlainText
55 }
56 }
57
58 pub const fn extension(&self) -> &'static str {
60 match self {
61 Self::ASS => "ass",
62 Self::SSA => "ssa",
63 Self::SRT => "srt",
64 Self::WebVTT => "vtt",
65 Self::PlainText => "txt",
66 }
67 }
68}
69
70#[derive(Debug, Clone)]
72pub struct ConversionOptions {
73 pub preserve_styling: bool,
75
76 pub preserve_positioning: bool,
78
79 pub inline_karaoke: bool,
81
82 pub strip_formatting: bool,
84
85 pub format_options: FormatOptions,
87}
88
89impl Default for ConversionOptions {
90 fn default() -> Self {
91 Self {
92 preserve_styling: true,
93 preserve_positioning: true,
94 inline_karaoke: false,
95 strip_formatting: false,
96 format_options: FormatOptions::default(),
97 }
98 }
99}
100
101#[derive(Debug, Clone)]
103pub enum FormatOptions {
104 None,
106
107 SRT {
109 include_numbers: bool,
111 millisecond_precision: bool,
113 },
114
115 WebVTT {
117 include_style_block: bool,
119 include_notes: bool,
121 use_cue_settings: bool,
123 },
124}
125
126impl Default for FormatOptions {
127 fn default() -> Self {
128 Self::None
129 }
130}
131
132pub struct FormatConverter;
134
135impl FormatConverter {
136 pub fn import(content: &str, format: Option<SubtitleFormat>) -> Result<String> {
138 let detected_format = format.unwrap_or_else(|| SubtitleFormat::from_content(content));
139
140 match detected_format {
141 SubtitleFormat::ASS | SubtitleFormat::SSA => {
142 Ok(content.to_string())
144 }
145 SubtitleFormat::SRT => Self::import_srt(content),
146 SubtitleFormat::WebVTT => Self::import_webvtt(content),
147 SubtitleFormat::PlainText => Self::import_plain_text(content),
148 }
149 }
150
151 pub fn export(
153 document: &EditorDocument,
154 format: SubtitleFormat,
155 options: &ConversionOptions,
156 ) -> Result<String> {
157 match format {
158 SubtitleFormat::ASS => Ok(document.text()),
159 SubtitleFormat::SSA => Self::export_ssa(document, options),
160 SubtitleFormat::SRT => Self::export_srt(document, options),
161 SubtitleFormat::WebVTT => Self::export_webvtt(document, options),
162 SubtitleFormat::PlainText => Self::export_plain_text(document, options),
163 }
164 }
165
166 fn import_srt(content: &str) -> Result<String> {
168 let mut output = String::new();
169
170 output.push_str("[Script Info]\n");
172 output.push_str("Title: Imported from SRT\n");
173 output.push_str("ScriptType: v4.00+\n");
174 output.push_str("WrapStyle: 0\n");
175 output.push_str("PlayResX: 640\n");
176 output.push_str("PlayResY: 480\n");
177 output.push_str("ScaledBorderAndShadow: yes\n\n");
178
179 output.push_str("[V4+ Styles]\n");
181 output.push_str("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n");
182 output.push_str("Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1\n\n");
183
184 output.push_str("[Events]\n");
186 output.push_str(
187 "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n",
188 );
189
190 let entries = Self::parse_srt_entries(content)?;
192 for entry in entries {
193 output.push_str(&format!(
194 "Dialogue: 0,{},{},Default,,0,0,0,,{}\n",
195 entry.start, entry.end, entry.text
196 ));
197 }
198
199 Ok(output)
200 }
201
202 fn parse_srt_entries(content: &str) -> Result<Vec<SrtEntry>> {
204 let mut entries = Vec::new();
205 let mut current_entry: Option<SrtEntry> = None;
206 let mut in_text = false;
207
208 for line in content.lines() {
209 let line = line.trim();
210
211 if line.is_empty() {
212 if let Some(entry) = current_entry.take() {
213 entries.push(entry);
214 }
215 in_text = false;
216 continue;
217 }
218
219 if line.chars().all(|c| c.is_ascii_digit()) && !in_text {
221 current_entry = Some(SrtEntry::default());
223 continue;
224 }
225
226 if line.contains("-->") {
228 if let Some(ref mut entry) = current_entry {
229 let parts: Vec<&str> = line.split("-->").collect();
230 if parts.len() == 2 {
231 entry.start = Self::parse_srt_time(parts[0].trim())?;
232 entry.end = Self::parse_srt_time(parts[1].trim())?;
233 in_text = true;
234 }
235 }
236 continue;
237 }
238
239 if in_text {
241 if let Some(ref mut entry) = current_entry {
242 if !entry.text.is_empty() {
243 entry.text.push_str("\\N");
244 }
245 let converted_text = Self::convert_srt_formatting(line);
247 entry.text.push_str(&converted_text);
248 }
249 }
250 }
251
252 if let Some(entry) = current_entry {
254 entries.push(entry);
255 }
256
257 Ok(entries)
258 }
259
260 fn parse_srt_time(time: &str) -> Result<String> {
262 let time = time.replace(',', ".");
266 let parts: Vec<&str> = time.split(':').collect();
267
268 if parts.len() != 3 {
269 return Err(EditorError::ValidationError {
270 message: format!("Invalid SRT timestamp: {time}"),
271 });
272 }
273
274 let hours: u32 = parts[0].parse().map_err(|_| EditorError::ValidationError {
275 message: format!("Invalid hours in timestamp: {}", parts[0]),
276 })?;
277
278 let minutes: u32 = parts[1].parse().map_err(|_| EditorError::ValidationError {
279 message: format!("Invalid minutes in timestamp: {}", parts[1]),
280 })?;
281
282 let seconds_parts: Vec<&str> = parts[2].split('.').collect();
283 let seconds: u32 = seconds_parts[0]
284 .parse()
285 .map_err(|_| EditorError::ValidationError {
286 message: format!("Invalid seconds in timestamp: {}", seconds_parts[0]),
287 })?;
288
289 let centiseconds = if seconds_parts.len() > 1 {
290 let millis: u32 = seconds_parts[1].parse().unwrap_or(0);
292 millis / 10
293 } else {
294 0
295 };
296
297 Ok(format!(
298 "{hours}:{minutes:02}:{seconds:02}.{centiseconds:02}"
299 ))
300 }
301
302 fn convert_srt_formatting(text: &str) -> String {
304 let mut result = text.to_string();
305
306 result = result.replace("<i>", "{\\i1}");
308 result = result.replace("</i>", "{\\i0}");
309 result = result.replace("<b>", "{\\b1}");
310 result = result.replace("</b>", "{\\b0}");
311 result = result.replace("<u>", "{\\u1}");
312 result = result.replace("</u>", "{\\u0}");
313
314 #[cfg(feature = "formats")]
316 {
317 result = regex::Regex::new(r"<[^>]+>")
318 .unwrap()
319 .replace_all(&result, "")
320 .to_string();
321 }
322
323 result
324 }
325
326 fn import_webvtt(content: &str) -> Result<String> {
328 let mut output = String::new();
329
330 output.push_str("[Script Info]\n");
332 output.push_str("Title: Imported from WebVTT\n");
333 output.push_str("ScriptType: v4.00+\n");
334 output.push_str("WrapStyle: 0\n");
335 output.push_str("PlayResX: 640\n");
336 output.push_str("PlayResY: 480\n");
337 output.push_str("ScaledBorderAndShadow: yes\n\n");
338
339 output.push_str("[V4+ Styles]\n");
341 output.push_str("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n");
342 output.push_str("Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1\n\n");
343
344 output.push_str("[Events]\n");
346 output.push_str(
347 "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n",
348 );
349
350 let cues = Self::parse_webvtt_cues(content)?;
352 for cue in cues {
353 output.push_str(&format!(
354 "Dialogue: 0,{},{},Default,,0,0,0,,{}\n",
355 cue.start, cue.end, cue.text
356 ));
357 }
358
359 Ok(output)
360 }
361
362 fn parse_webvtt_cues(content: &str) -> Result<Vec<WebVttCue>> {
364 let mut cues = Vec::new();
365 let mut current_cue: Option<WebVttCue> = None;
366 let mut in_cue = false;
367
368 for line in content.lines() {
369 let line = line.trim();
370
371 if line.starts_with("WEBVTT") || line.starts_with("NOTE") || line.is_empty() {
373 if let Some(cue) = current_cue.take() {
374 cues.push(cue);
375 }
376 in_cue = false;
377 continue;
378 }
379
380 if line.contains("-->") {
382 current_cue = Some(WebVttCue::default());
383 if let Some(ref mut cue) = current_cue {
384 let parts: Vec<&str> = line.split("-->").collect();
385 if parts.len() >= 2 {
386 cue.start = Self::parse_webvtt_time(parts[0].trim())?;
387 cue.end = Self::parse_webvtt_time(parts[1].trim())?;
388 in_cue = true;
389 }
390 }
391 continue;
392 }
393
394 if in_cue {
396 if let Some(ref mut cue) = current_cue {
397 if !cue.text.is_empty() {
398 cue.text.push_str("\\N");
399 }
400 let converted_text = Self::convert_webvtt_formatting(line);
401 cue.text.push_str(&converted_text);
402 }
403 }
404 }
405
406 if let Some(cue) = current_cue {
408 cues.push(cue);
409 }
410
411 Ok(cues)
412 }
413
414 fn parse_webvtt_time(time: &str) -> Result<String> {
416 let parts: Vec<&str> = time.split(':').collect();
420
421 let (hours, minutes, seconds_str) = if parts.len() == 3 {
422 (parts[0].parse::<u32>().unwrap_or(0), parts[1], parts[2])
424 } else if parts.len() == 2 {
425 (0, parts[0], parts[1])
427 } else {
428 return Err(EditorError::ValidationError {
429 message: format!("Invalid WebVTT timestamp: {time}"),
430 });
431 };
432
433 let minutes: u32 = minutes.parse().map_err(|_| EditorError::ValidationError {
434 message: format!("Invalid minutes in timestamp: {minutes}"),
435 })?;
436
437 let seconds_parts: Vec<&str> = seconds_str.split('.').collect();
438 let seconds: u32 = seconds_parts[0]
439 .parse()
440 .map_err(|_| EditorError::ValidationError {
441 message: format!("Invalid seconds in timestamp: {}", seconds_parts[0]),
442 })?;
443
444 let centiseconds = if seconds_parts.len() > 1 {
445 let millis: u32 = seconds_parts[1].parse().unwrap_or(0);
447 millis / 10
448 } else {
449 0
450 };
451
452 Ok(format!(
453 "{hours}:{minutes:02}:{seconds:02}.{centiseconds:02}"
454 ))
455 }
456
457 fn convert_webvtt_formatting(text: &str) -> String {
459 let mut result = text.to_string();
460
461 result = result.replace("<i>", "{\\i1}");
463 result = result.replace("</i>", "{\\i0}");
464 result = result.replace("<b>", "{\\b1}");
465 result = result.replace("</b>", "{\\b0}");
466 result = result.replace("<u>", "{\\u1}");
467 result = result.replace("</u>", "{\\u0}");
468
469 result = regex::Regex::new(r"<v\s+([^>]+)>")
471 .unwrap()
472 .replace_all(&result, "")
473 .to_string();
474 result = result.replace("</v>", "");
475
476 result = regex::Regex::new(r"<[^>]+>")
478 .unwrap()
479 .replace_all(&result, "")
480 .to_string();
481
482 result
483 }
484
485 fn import_plain_text(content: &str) -> Result<String> {
487 let mut output = String::new();
488
489 output.push_str("[Script Info]\n");
491 output.push_str("Title: Imported from Plain Text\n");
492 output.push_str("ScriptType: v4.00+\n\n");
493
494 output.push_str("[V4+ Styles]\n");
495 output.push_str("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n");
496 output.push_str("Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1\n\n");
497
498 output.push_str("[Events]\n");
499 output.push_str(
500 "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n",
501 );
502
503 let text = content.lines().collect::<Vec<_>>().join("\\N");
505 output.push_str(&format!(
506 "Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,{text}\n"
507 ));
508
509 Ok(output)
510 }
511
512 fn export_ssa(document: &EditorDocument, _options: &ConversionOptions) -> Result<String> {
514 let content = document.text();
516 let mut output = content.replace("[V4+ Styles]", "[V4 Styles]");
517 output = output.replace("ScriptType: v4.00+", "ScriptType: v4.00");
518 Ok(output)
519 }
520
521 fn export_srt(document: &EditorDocument, options: &ConversionOptions) -> Result<String> {
523 let mut output = String::new();
524 let mut index = 1;
525
526 document.parse_script_with(|script| {
527 for section in script.sections() {
528 if let ass_core::parser::ast::Section::Events(events) = section {
529 for event in events {
530 if event.event_type == EventType::Dialogue {
531 output.push_str(&format!("{index}\n"));
533 index += 1;
534
535 let start = Self::ass_time_to_srt(event.start);
537 let end = Self::ass_time_to_srt(event.end);
538 output.push_str(&format!("{start} --> {end}\n"));
539
540 let text = if options.strip_formatting {
542 Self::strip_ass_tags(event.text)
543 } else {
544 Self::convert_ass_to_srt_formatting(event.text)
545 };
546 output.push_str(&text.replace("\\N", "\n"));
547 output.push_str("\n\n");
548 }
549 }
550 }
551 }
552 })?;
553
554 Ok(output)
555 }
556
557 fn ass_time_to_srt(time: &str) -> String {
559 let parts: Vec<&str> = time.split(':').collect();
563 if parts.len() != 3 {
564 return time.to_string();
565 }
566
567 let hours = format!("{:02}", parts[0].parse::<u32>().unwrap_or(0));
568 let minutes = parts[1];
569
570 let seconds_parts: Vec<&str> = parts[2].split('.').collect();
571 let seconds = seconds_parts[0];
572 let centiseconds = seconds_parts.get(1).unwrap_or(&"00");
573
574 let millis = centiseconds.parse::<u32>().unwrap_or(0) * 10;
576
577 format!("{hours}:{minutes}:{seconds},{millis:03}")
578 }
579
580 fn convert_ass_to_srt_formatting(text: &str) -> String {
582 let mut result = text.to_string();
583
584 result = result.replace("{\\i1}", "<i>");
586 result = result.replace("{\\i0}", "</i>");
587 result = result.replace("{\\b1}", "<b>");
588 result = result.replace("{\\b0}", "</b>");
589 result = result.replace("{\\u1}", "<u>");
590 result = result.replace("{\\u0}", "</u>");
591
592 while let Some(start) = result.find('{') {
594 if let Some(end) = result[start..].find('}') {
595 result.replace_range(start..start + end + 1, "");
596 } else {
597 break;
598 }
599 }
600
601 result
602 }
603
604 fn strip_ass_tags(text: &str) -> String {
606 let mut result = text.to_string();
607 while let Some(start) = result.find('{') {
608 if let Some(end) = result[start..].find('}') {
609 result.replace_range(start..start + end + 1, "");
610 } else {
611 break;
612 }
613 }
614 result
615 }
616
617 fn export_webvtt(document: &EditorDocument, options: &ConversionOptions) -> Result<String> {
619 let mut output = String::new();
620
621 output.push_str("WEBVTT\n\n");
623
624 if let FormatOptions::WebVTT {
626 include_style_block: true,
627 ..
628 } = &options.format_options
629 {
630 output.push_str("STYLE\n");
631 output.push_str("::cue {\n");
632 output
633 .push_str(" background-image: linear-gradient(to bottom, dimgray, lightgray);\n");
634 output.push_str(" color: papayawhip;\n");
635 output.push_str("}\n\n");
636 }
637
638 document.parse_script_with(|script| {
639 for section in script.sections() {
640 if let ass_core::parser::ast::Section::Events(events) = section {
641 for event in events {
642 if event.event_type == EventType::Dialogue {
643 let start = Self::ass_time_to_webvtt(event.start);
645 let end = Self::ass_time_to_webvtt(event.end);
646 output.push_str(&format!("{start} --> {end}"));
647
648 if let FormatOptions::WebVTT {
650 use_cue_settings: true,
651 ..
652 } = &options.format_options
653 {
654 let margin_v: i32 = event.margin_v.parse().unwrap_or(0);
656 if margin_v != 0 {
657 output.push_str(&format!(" line:{}", 100 - margin_v));
658 }
659 }
660
661 output.push('\n');
662
663 let text = if options.strip_formatting {
665 Self::strip_ass_tags(event.text)
666 } else {
667 Self::convert_ass_to_webvtt_formatting(event.text)
668 };
669 output.push_str(&text.replace("\\N", "\n"));
670 output.push_str("\n\n");
671 }
672 }
673 }
674 }
675 })?;
676
677 Ok(output)
678 }
679
680 fn ass_time_to_webvtt(time: &str) -> String {
682 let parts: Vec<&str> = time.split(':').collect();
686 if parts.len() != 3 {
687 return time.to_string();
688 }
689
690 let hours = format!("{:02}", parts[0].parse::<u32>().unwrap_or(0));
691 let minutes = parts[1];
692
693 let seconds_parts: Vec<&str> = parts[2].split('.').collect();
694 let seconds = seconds_parts[0];
695 let centiseconds = seconds_parts.get(1).unwrap_or(&"00");
696
697 let millis = centiseconds.parse::<u32>().unwrap_or(0) * 10;
699
700 format!("{hours}:{minutes}:{seconds}.{millis:03}")
701 }
702
703 fn convert_ass_to_webvtt_formatting(text: &str) -> String {
705 let mut result = text.to_string();
706
707 result = result.replace("{\\i1}", "<i>");
709 result = result.replace("{\\i0}", "</i>");
710 result = result.replace("{\\b1}", "<b>");
711 result = result.replace("{\\b0}", "</b>");
712 result = result.replace("{\\u1}", "<u>");
713 result = result.replace("{\\u0}", "</u>");
714
715 while let Some(start) = result.find('{') {
717 if let Some(end) = result[start..].find('}') {
718 result.replace_range(start..start + end + 1, "");
719 } else {
720 break;
721 }
722 }
723
724 result
725 }
726
727 fn export_plain_text(document: &EditorDocument, options: &ConversionOptions) -> Result<String> {
729 let mut output = String::new();
730
731 document.parse_script_with(|script| {
732 for section in script.sections() {
733 if let ass_core::parser::ast::Section::Events(events) = section {
734 for event in events {
735 if event.event_type == EventType::Dialogue {
736 let text = if options.strip_formatting {
737 Self::strip_ass_tags(event.text)
738 } else {
739 event.text.to_string()
740 };
741 output.push_str(&text.replace("\\N", "\n"));
742 output.push('\n');
743 }
744 }
745 }
746 }
747 })?;
748
749 Ok(output)
750 }
751}
752
753#[derive(Default)]
755struct SrtEntry {
756 start: String,
757 end: String,
758 text: String,
759}
760
761#[derive(Default)]
763struct WebVttCue {
764 start: String,
765 end: String,
766 text: String,
767}
768
769#[cfg(feature = "std")]
771pub fn import_from_file(path: &str) -> Result<EditorDocument> {
772 use std::fs;
773
774 let content = fs::read_to_string(path).map_err(|e| EditorError::IoError(e.to_string()))?;
775
776 let format = path
777 .rfind('.')
778 .and_then(|pos| SubtitleFormat::from_extension(&path[pos + 1..]));
779
780 let ass_content = FormatConverter::import(&content, format)?;
781 EditorDocument::from_content(&ass_content)
782}
783
784#[cfg(feature = "std")]
786pub fn export_to_file(
787 document: &EditorDocument,
788 path: &str,
789 format: Option<SubtitleFormat>,
790 options: &ConversionOptions,
791) -> Result<()> {
792 use std::fs;
793
794 let detected_format = format
795 .or_else(|| {
796 path.rfind('.')
797 .and_then(|pos| SubtitleFormat::from_extension(&path[pos + 1..]))
798 })
799 .unwrap_or(SubtitleFormat::ASS);
800
801 let content = FormatConverter::export(document, detected_format, options)?;
802
803 fs::write(path, content).map_err(|e| EditorError::IoError(e.to_string()))?;
804
805 Ok(())
806}
807
808#[cfg(test)]
809mod tests {
810 use super::*;
811 #[cfg(not(feature = "std"))]
812 use alloc::string::ToString;
813 #[cfg(not(feature = "std"))]
814 use alloc::{format, string::String};
815
816 #[test]
817 fn test_format_detection() {
818 assert_eq!(
819 SubtitleFormat::from_extension("ass"),
820 Some(SubtitleFormat::ASS)
821 );
822 assert_eq!(
823 SubtitleFormat::from_extension("srt"),
824 Some(SubtitleFormat::SRT)
825 );
826 assert_eq!(
827 SubtitleFormat::from_extension("vtt"),
828 Some(SubtitleFormat::WebVTT)
829 );
830 assert_eq!(SubtitleFormat::from_extension("unknown"), None);
831
832 assert_eq!(
833 SubtitleFormat::from_content("[Script Info]\nTitle: Test"),
834 SubtitleFormat::ASS
835 );
836 assert_eq!(
837 SubtitleFormat::from_content("WEBVTT\n\n00:00.000 --> 00:05.000"),
838 SubtitleFormat::WebVTT
839 );
840 assert_eq!(
841 SubtitleFormat::from_content("1\n00:00:00,000 --> 00:00:05,000\nHello"),
842 SubtitleFormat::SRT
843 );
844 }
845
846 #[test]
847 fn test_srt_import() {
848 let srt_content = r#"1
84900:00:00,000 --> 00:00:05,000
850Hello <i>world</i>!
851
8522
85300:00:05,000 --> 00:00:10,000
854This is a <b>test</b>."#;
855
856 let result = FormatConverter::import(srt_content, Some(SubtitleFormat::SRT)).unwrap();
857
858 assert!(result.contains("[Script Info]"));
859 assert!(result.contains("[Events]"));
860 assert!(result.contains("Hello {\\i1}world{\\i0}!"));
861 assert!(result.contains("This is a {\\b1}test{\\b0}."));
862 }
863
864 #[test]
865 fn test_webvtt_import() {
866 let webvtt_content = r#"WEBVTT
867
86800:00:00.000 --> 00:00:05.000
869Hello <i>world</i>!
870
87100:00:05.000 --> 00:00:10.000
872This is a test."#;
873
874 let result = FormatConverter::import(webvtt_content, Some(SubtitleFormat::WebVTT)).unwrap();
875
876 assert!(result.contains("[Script Info]"));
877 assert!(result.contains("[Events]"));
878 assert!(result.contains("Hello {\\i1}world{\\i0}!"));
879 }
880
881 #[test]
882 fn test_export_srt() {
883 let doc = EditorDocument::from_content(
884 r#"[Script Info]
885Title: Test
886
887[V4+ Styles]
888Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
889Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
890
891[Events]
892Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
893Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Hello {\i1}world{\i0}!
894Dialogue: 0,0:00:05.00,0:00:10.00,Default,,0,0,0,,Test line\NSecond line"#
895 ).unwrap();
896
897 let options = ConversionOptions::default();
898 let result = FormatConverter::export(&doc, SubtitleFormat::SRT, &options).unwrap();
899
900 assert!(result.contains("1\n00:00:00,000 --> 00:00:05,000"));
901 assert!(result.contains("Hello <i>world</i>!"));
902 assert!(result.contains("Test line\nSecond line"));
903 }
904
905 #[test]
906 fn test_export_webvtt() {
907 let doc = EditorDocument::from_content(
908 r#"[Script Info]
909Title: Test
910
911[Events]
912Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
913Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Hello world!"#,
914 )
915 .unwrap();
916
917 let options = ConversionOptions::default();
918 let result = FormatConverter::export(&doc, SubtitleFormat::WebVTT, &options).unwrap();
919
920 assert!(result.starts_with("WEBVTT"));
921 assert!(result.contains("00:00:00.000 --> 00:00:05.000"));
922 assert!(result.contains("Hello world!"));
923 }
924
925 #[test]
926 fn test_strip_formatting() {
927 let doc = EditorDocument::from_content(
928 r#"[Events]
929Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
930Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,{\i1}Hello{\i0} {\b1}world{\b0}!"#,
931 )
932 .unwrap();
933
934 let options = ConversionOptions {
935 strip_formatting: true,
936 ..Default::default()
937 };
938
939 let result = FormatConverter::export(&doc, SubtitleFormat::SRT, &options).unwrap();
940 assert!(result.contains("Hello world!"));
941 assert!(!result.contains("<i>"));
942 assert!(!result.contains("<b>"));
943 }
944}