1use crate::core::{EditorDocument, EditorError};
7use crate::formats::{
8 Format, FormatExporter, FormatImporter, FormatInfo, FormatOptions, FormatResult,
9};
10use ass_core::parser::Script;
11use std::collections::HashMap;
12use std::io::{Read, Write};
13
14#[derive(Debug)]
16pub struct WebVttFormat {
17 info: FormatInfo,
18}
19
20impl WebVttFormat {
21 pub fn new() -> Self {
23 Self {
24 info: FormatInfo {
25 name: "WebVTT".to_string(),
26 extensions: vec!["vtt".to_string(), "webvtt".to_string()],
27 mime_type: "text/vtt".to_string(),
28 description: "WebVTT subtitle format with full style and positioning preservation"
29 .to_string(),
30 supports_styling: true,
31 supports_positioning: true,
32 },
33 }
34 }
35
36 fn parse_vtt_time(time_str: &str) -> Result<String, EditorError> {
38 let time_str = time_str.trim();
39
40 let parts: Vec<&str> = time_str.split('.').collect();
42 if parts.len() != 2 {
43 return Err(EditorError::InvalidFormat(format!(
44 "Invalid WebVTT time format: {time_str}"
45 )));
46 }
47
48 let time_part = parts[0];
49 let ms_part = parts[1];
50
51 let ms: u32 = ms_part
53 .parse()
54 .map_err(|_| EditorError::InvalidFormat(format!("Invalid milliseconds: {ms_part}")))?;
55 let cs = ms / 10; let time_components: Vec<&str> = time_part.split(':').collect();
59 let ass_time = match time_components.len() {
60 2 => {
61 format!("0:{time_part}.{cs:02}")
63 }
64 3 => {
65 let hours = time_components[0];
67 let hours = if hours.starts_with('0') && hours.len() > 1 {
68 &hours[1..]
69 } else {
70 hours
71 };
72 format!(
73 "{hours}:{}:{}.{cs:02}",
74 time_components[1], time_components[2]
75 )
76 }
77 _ => {
78 return Err(EditorError::InvalidFormat(format!(
79 "Invalid WebVTT time format: {time_str}"
80 )));
81 }
82 };
83
84 Ok(ass_time)
85 }
86
87 fn format_vtt_time(ass_time: &str) -> Result<String, EditorError> {
89 let ass_time = ass_time.trim();
90
91 if let Some(dot_pos) = ass_time.find('.') {
93 let (time_part, cs_part) = ass_time.split_at(dot_pos);
94 let cs_part = &cs_part[1..]; let cs: u32 = cs_part.parse().map_err(|_| {
98 EditorError::InvalidFormat(format!("Invalid centiseconds: {cs_part}"))
99 })?;
100 let ms = cs * 10; let parts: Vec<&str> = time_part.split(':').collect();
104 if parts.len() == 3 {
105 let hours: u32 = parts[0].parse().map_err(|_| {
106 EditorError::InvalidFormat(format!("Invalid hours: {}", parts[0]))
107 })?;
108 Ok(format!("{hours:02}:{}:{}.{ms:03}", parts[1], parts[2]))
109 } else {
110 Err(EditorError::InvalidFormat(format!(
111 "Invalid ASS time format: {ass_time}"
112 )))
113 }
114 } else {
115 Err(EditorError::InvalidFormat(format!(
116 "Invalid ASS time format: {ass_time}"
117 )))
118 }
119 }
120
121 fn convert_vtt_to_ass_styling(text: &str) -> String {
123 let mut result = text.to_string();
124
125 result = result.replace("<b>", r"{\b1}");
127 result = result.replace("</b>", r"{\b0}");
128 result = result.replace("<i>", r"{\i1}");
129 result = result.replace("</i>", r"{\i0}");
130 result = result.replace("<u>", r"{\u1}");
131 result = result.replace("</u>", r"{\u0}");
132
133 #[cfg(feature = "formats")]
135 {
136 let class_regex = regex::Regex::new(r#"<c\.([^>]+)>([^<]*)</c>"#).unwrap();
137 result = class_regex
138 .replace_all(&result, r"{\c&H$1&}$2{\c}")
139 .to_string();
140
141 let voice_regex = regex::Regex::new(r#"<v\s+([^>]+)>([^<]*)</v>"#).unwrap();
143 result = voice_regex
144 .replace_all(&result, r"{\fn$1}$2{\fn}")
145 .to_string();
146
147 let ruby_regex = regex::Regex::new(r#"<ruby>([^<]*)<rt>([^<]*)</rt></ruby>"#).unwrap();
149 result = ruby_regex.replace_all(&result, "$1($2)").to_string();
150
151 let timestamp_regex = regex::Regex::new(r#"<([0-9:.,]+)>"#).unwrap();
153 result = timestamp_regex.replace_all(&result, "").to_string();
154 }
155
156 result
157 }
158
159 fn convert_ass_to_vtt_styling(text: &str) -> String {
161 let mut result = text.to_string();
162
163 result = result.replace(r"{\b1}", "<b>");
165 result = result.replace(r"{\b0}", "</b>");
166 result = result.replace(r"{\i1}", "<i>");
167 result = result.replace(r"{\i0}", "</i>");
168 result = result.replace(r"{\u1}", "<u>");
169 result = result.replace(r"{\u0}", "</u>");
170
171 #[cfg(feature = "formats")]
172 {
173 let color_regex = regex::Regex::new(r"\\c&H([0-9A-Fa-f]{6})&").unwrap();
175 result = color_regex.replace_all(&result, r#"<c.$1>"#).to_string();
176 result = result.replace(r"{\c}", "</c>");
177
178 let font_regex = regex::Regex::new(r"\\fn([^}]+)").unwrap();
180 result = font_regex.replace_all(&result, r#"<v $1>"#).to_string();
181 result = result.replace(r"{\fn}", "</v>");
182
183 let pos_regex = regex::Regex::new(r"\\pos\(([^,]+),([^)]+)\)").unwrap();
185 result = pos_regex.replace_all(&result, "").to_string(); let cleanup_regex = regex::Regex::new(r"\{[^}]*\}").unwrap();
189 result = cleanup_regex.replace_all(&result, "").to_string();
190 }
191
192 result
193 }
194
195 fn parse_cue_settings(settings: &str) -> HashMap<String, String> {
197 let mut cue_settings = HashMap::new();
198
199 for setting in settings.split_whitespace() {
200 if let Some(colon_pos) = setting.find(':') {
201 let (key, value) = setting.split_at(colon_pos);
202 let value = &value[1..]; cue_settings.insert(key.to_string(), value.to_string());
204 }
205 }
206
207 cue_settings
208 }
209
210 fn cue_settings_to_ass_positioning(settings: &HashMap<String, String>) -> String {
212 let mut ass_tags = String::new();
213
214 if let (Some(line), Some(position)) = (settings.get("line"), settings.get("position")) {
216 if let (Ok(line_val), Ok(pos_val)) = (line.parse::<f32>(), position.parse::<f32>()) {
218 let x = (pos_val * 640.0) as u32; let y = (line_val * 480.0) as u32;
220 ass_tags.push_str(&format!(r"\pos({x},{y})"));
221 }
222 }
223
224 if let Some(align) = settings.get("align") {
226 let alignment = match align.as_str() {
227 "start" | "left" => 1,
228 "center" | "middle" => 2,
229 "end" | "right" => 3,
230 _ => 2, };
232 ass_tags.push_str(&format!(r"\an{alignment}"));
233 }
234
235 if !ass_tags.is_empty() {
236 format!("{{{ass_tags}}}")
237 } else {
238 String::new()
239 }
240 }
241
242 fn parse_vtt_cue(lines: &[String], start_idx: usize) -> Result<(usize, String), EditorError> {
244 if start_idx >= lines.len() {
245 return Err(EditorError::InvalidFormat(
246 "Unexpected end of file".to_string(),
247 ));
248 }
249
250 let mut idx = start_idx;
251
252 while idx < lines.len() {
254 let line = lines[idx].trim();
255 if line.is_empty() || line.starts_with("NOTE") {
256 idx += 1;
257 continue;
258 }
259 break;
260 }
261
262 if idx >= lines.len() {
263 return Err(EditorError::InvalidFormat(
264 "Unexpected end of file".to_string(),
265 ));
266 }
267
268 let current_line = &lines[idx];
269
270 if current_line.contains("-->") {
272 let timestamp_line = current_line;
274 let parts: Vec<&str> = timestamp_line.split("-->").collect();
275 if parts.len() < 2 {
276 return Err(EditorError::InvalidFormat(format!(
277 "Invalid timestamp format: {timestamp_line}"
278 )));
279 }
280
281 let start_time = Self::parse_vtt_time(parts[0])?;
282 let end_time_and_settings: Vec<&str> = parts[1].split_whitespace().collect();
283 let end_time = Self::parse_vtt_time(end_time_and_settings[0])?;
284
285 let cue_settings = if end_time_and_settings.len() > 1 {
287 let settings_str = end_time_and_settings[1..].join(" ");
288 Self::parse_cue_settings(&settings_str)
289 } else {
290 HashMap::new()
291 };
292
293 idx += 1;
294
295 let mut text_lines = Vec::new();
297 while idx < lines.len() && !lines[idx].trim().is_empty() {
298 let styled_text = Self::convert_vtt_to_ass_styling(&lines[idx]);
299 text_lines.push(styled_text);
300 idx += 1;
301 }
302
303 if text_lines.is_empty() {
304 return Err(EditorError::InvalidFormat("Empty cue text".to_string()));
305 }
306
307 let text = text_lines.join("\\N"); let positioning = Self::cue_settings_to_ass_positioning(&cue_settings);
309 let dialogue_line =
310 format!("Dialogue: 0,{start_time},{end_time},Default,,0,0,0,,{positioning}{text}");
311
312 Ok((idx, dialogue_line))
313 } else {
314 idx += 1;
316 if idx < lines.len() && lines[idx].contains("-->") {
317 Self::parse_vtt_cue(lines, idx)
318 } else {
319 Err(EditorError::InvalidFormat(format!(
320 "Expected timestamp line after cue identifier: {current_line}"
321 )))
322 }
323 }
324 }
325}
326
327impl Default for WebVttFormat {
328 fn default() -> Self {
329 Self::new()
330 }
331}
332
333impl FormatImporter for WebVttFormat {
334 fn format_info(&self) -> &FormatInfo {
335 &self.info
336 }
337
338 fn import_from_reader(
339 &self,
340 reader: &mut dyn Read,
341 options: &FormatOptions,
342 ) -> Result<(EditorDocument, FormatResult), EditorError> {
343 let mut content = String::new();
345 reader
346 .read_to_string(&mut content)
347 .map_err(|e| EditorError::IoError(format!("Failed to read WebVTT content: {e}")))?;
348
349 let lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
350 let mut warnings = Vec::new();
351 let mut dialogues = Vec::new();
352 let mut idx = 0;
353 let mut cue_count = 0;
354
355 if lines.is_empty() || !lines[0].trim().starts_with("WEBVTT") {
357 warnings.push("Missing or invalid WebVTT header".to_string());
358 } else {
359 idx = 1; }
361
362 while idx < lines.len() {
364 match Self::parse_vtt_cue(&lines, idx) {
365 Ok((next_idx, dialogue)) => {
366 dialogues.push(dialogue);
367 idx = next_idx;
368 cue_count += 1;
369 }
370 Err(e) => {
371 if idx < lines.len() {
372 warnings.push(format!("Skipping invalid cue at line {}: {e}", idx + 1));
373 idx += 1;
374 } else {
375 break;
376 }
377 }
378 }
379 }
380
381 let mut ass_content = String::new();
383
384 ass_content.push_str("[Script Info]\n");
386 ass_content.push_str("Title: Converted from WebVTT\n");
387 ass_content.push_str("ScriptType: v4.00+\n");
388 ass_content.push_str("Collisions: Normal\n");
389 ass_content.push_str("PlayDepth: 0\n");
390 ass_content.push_str("Timer: 100.0000\n");
391 ass_content.push_str("Video Aspect Ratio: 0\n");
392 ass_content.push_str("Video Zoom: 6\n");
393 ass_content.push_str("Video Position: 0\n");
394 ass_content.push_str("PlayResX: 640\n");
395 ass_content.push_str("PlayResY: 480\n\n");
396
397 ass_content.push_str("[V4+ Styles]\n");
399 ass_content.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");
400 ass_content.push_str("Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H80000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1\n\n");
401
402 ass_content.push_str("[Events]\n");
404 ass_content.push_str(
405 "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n",
406 );
407
408 for dialogue in dialogues {
409 ass_content.push_str(&dialogue);
410 ass_content.push('\n');
411 }
412
413 let _script = Script::parse(&ass_content)?;
415
416 let document = EditorDocument::from_content(&ass_content)?;
418
419 let mut result = FormatResult::success(cue_count)
421 .with_metadata("original_format".to_string(), "WebVTT".to_string())
422 .with_metadata("cues_count".to_string(), cue_count.to_string())
423 .with_metadata("encoding".to_string(), options.encoding.clone());
424
425 if !warnings.is_empty() {
426 result = result.with_warnings(warnings);
427 }
428
429 Ok((document, result))
430 }
431}
432
433impl FormatExporter for WebVttFormat {
434 fn format_info(&self) -> &FormatInfo {
435 &self.info
436 }
437
438 fn export_to_writer(
439 &self,
440 document: &EditorDocument,
441 writer: &mut dyn Write,
442 options: &FormatOptions,
443 ) -> Result<FormatResult, EditorError> {
444 let events = document.parse_script_with(|script| {
446 if let Some(ass_core::parser::ast::Section::Events(events)) =
448 script.find_section(ass_core::parser::ast::SectionType::Events)
449 {
450 events
452 .iter()
453 .map(|event| {
454 (
455 event.event_type,
456 event.start.to_string(),
457 event.end.to_string(),
458 event.text.to_string(),
459 )
460 })
461 .collect::<Vec<_>>()
462 } else {
463 Vec::new()
464 }
465 })?;
466
467 let mut vtt_content = String::new();
468 let mut cue_num = 1;
469 let mut warnings = Vec::new();
470
471 vtt_content.push_str("WEBVTT\n\n");
473
474 for (event_type, start, end, text) in &events {
475 if event_type.as_str() != "Dialogue" {
477 continue;
478 }
479
480 let start_time = match Self::format_vtt_time(start) {
482 Ok(time) => time,
483 Err(e) => {
484 warnings.push(format!("Invalid start time for cue {cue_num}: {e}"));
485 continue;
486 }
487 };
488
489 let end_time = match Self::format_vtt_time(end) {
490 Ok(time) => time,
491 Err(e) => {
492 warnings.push(format!("Invalid end time for cue {cue_num}: {e}"));
493 continue;
494 }
495 };
496
497 let mut text = text.clone();
499
500 text = text.replace("\\N", "\n");
502 text = text.replace("\\n", "\n");
503
504 text = Self::convert_ass_to_vtt_styling(&text);
506
507 vtt_content.push_str(&format!("{cue_num}\n"));
509 vtt_content.push_str(&format!("{start_time} --> {end_time}\n"));
510 vtt_content.push_str(&text);
511 vtt_content.push_str("\n\n");
512
513 cue_num += 1;
514 }
515
516 let bytes = if options.encoding.eq_ignore_ascii_case("UTF-8") {
518 vtt_content.into_bytes()
519 } else {
520 warnings.push(format!(
521 "Encoding '{}' not supported, using UTF-8 instead",
522 options.encoding
523 ));
524 vtt_content.into_bytes()
525 };
526
527 writer
528 .write_all(&bytes)
529 .map_err(|e| EditorError::IoError(format!("Failed to write WebVTT content: {e}")))?;
530
531 let mut result = FormatResult::success(cue_num - 1)
532 .with_metadata("exported_format".to_string(), "WebVTT".to_string())
533 .with_metadata("cues_exported".to_string(), (cue_num - 1).to_string());
534
535 if !warnings.is_empty() {
536 result = result.with_warnings(warnings);
537 }
538
539 Ok(result)
540 }
541}
542
543impl Format for WebVttFormat {
544 fn as_importer(&self) -> &dyn FormatImporter {
545 self
546 }
547
548 fn as_exporter(&self) -> &dyn FormatExporter {
549 self
550 }
551}
552
553#[cfg(test)]
554mod tests {
555 use super::*;
556 #[cfg(not(feature = "std"))]
557 use alloc::string::ToString;
558 #[cfg(not(feature = "std"))]
559 use alloc::{format, string::String, vec};
560
561 const SAMPLE_WEBVTT: &str = r#"WEBVTT
562
5631
56400:00:00.000 --> 00:00:05.000
565<b>Hello</b> <i>World</i>!
566
5672
56800:00:06.000 --> 00:00:10.000 align:center
569This is a <u>subtitle</u> with <c.red>red text</c>.
570
5713
57200:12:30.500 --> 00:15:45.750 line:20% position:50%
573<v Speaker>Multiple</v>
574lines with positioning
575
576"#;
577
578 #[test]
579 fn test_webvtt_format_creation() {
580 let format = WebVttFormat::new();
581 let info = FormatImporter::format_info(&format);
582 assert_eq!(info.name, "WebVTT");
583 assert!(info.supports_styling);
584 assert!(info.supports_positioning);
585 assert!(format.can_import("vtt"));
586 assert!(format.can_import("webvtt"));
587 assert!(format.can_export("vtt"));
588 }
589
590 #[test]
591 fn test_parse_vtt_time() {
592 assert_eq!(
593 WebVttFormat::parse_vtt_time("00:01:23.456").unwrap(),
594 "0:01:23.45"
595 );
596 assert_eq!(
597 WebVttFormat::parse_vtt_time("01:00:00.000").unwrap(),
598 "1:00:00.00"
599 );
600 assert_eq!(
601 WebVttFormat::parse_vtt_time("30:45.123").unwrap(),
602 "0:30:45.12"
603 );
604
605 assert!(WebVttFormat::parse_vtt_time("invalid").is_err());
606 assert!(WebVttFormat::parse_vtt_time("00:01:23").is_err());
607 }
608
609 #[test]
610 fn test_format_vtt_time() {
611 assert_eq!(
612 WebVttFormat::format_vtt_time("0:01:23.45").unwrap(),
613 "00:01:23.450"
614 );
615 assert_eq!(
616 WebVttFormat::format_vtt_time("1:00:00.00").unwrap(),
617 "01:00:00.000"
618 );
619 assert_eq!(
620 WebVttFormat::format_vtt_time("10:30:45.12").unwrap(),
621 "10:30:45.120"
622 );
623
624 assert!(WebVttFormat::format_vtt_time("invalid").is_err());
625 assert!(WebVttFormat::format_vtt_time("00:01:23").is_err());
626 }
627
628 #[test]
629 fn test_convert_vtt_to_ass_styling() {
630 assert_eq!(
631 WebVttFormat::convert_vtt_to_ass_styling("<b>Bold</b> text"),
632 r"{\b1}Bold{\b0} text"
633 );
634 assert_eq!(
635 WebVttFormat::convert_vtt_to_ass_styling("<i>Italic</i> and <u>underlined</u>"),
636 r"{\i1}Italic{\i0} and {\u1}underlined{\u0}"
637 );
638 }
639
640 #[test]
641 fn test_convert_ass_to_vtt_styling() {
642 assert_eq!(
643 WebVttFormat::convert_ass_to_vtt_styling(r"{\b1}Bold{\b0} text"),
644 "<b>Bold</b> text"
645 );
646 assert_eq!(
647 WebVttFormat::convert_ass_to_vtt_styling(r"{\i1}Italic{\i0} and {\u1}underlined{\u0}"),
648 "<i>Italic</i> and <u>underlined</u>"
649 );
650 }
651
652 #[test]
653 fn test_parse_cue_settings() {
654 let settings = WebVttFormat::parse_cue_settings("align:center line:20% position:50%");
655 assert_eq!(settings.get("align"), Some(&"center".to_string()));
656 assert_eq!(settings.get("line"), Some(&"20%".to_string()));
657 assert_eq!(settings.get("position"), Some(&"50%".to_string()));
658 }
659
660 #[test]
661 fn test_webvtt_import_from_string() {
662 let format = WebVttFormat::new();
663 let options = FormatOptions::default();
664
665 let result = format.import_from_string(SAMPLE_WEBVTT, &options);
666 assert!(result.is_ok());
667
668 let (document, format_result) = result.unwrap();
669 assert!(format_result.success);
670 assert_eq!(format_result.lines_processed, 3); assert!(document.text().contains("Hello"));
672 assert!(document.text().contains("World"));
673 assert!(document.text().contains(r"{\b1}"));
674 assert!(document.text().contains(r"{\i1}"));
675 }
676
677 #[test]
678 fn test_webvtt_export_to_string() {
679 let format = WebVttFormat::new();
680 let options = FormatOptions::default();
681
682 let (document, _) = format.import_from_string(SAMPLE_WEBVTT, &options).unwrap();
684
685 let result = format.export_to_string(&document, &options);
687 assert!(result.is_ok());
688
689 let (exported_content, format_result) = result.unwrap();
690 assert!(format_result.success);
691 assert!(exported_content.contains("WEBVTT"));
692 assert!(exported_content.contains("Hello"));
693 assert!(exported_content.contains("<b>"));
694 assert!(exported_content.contains("<i>"));
695 assert!(exported_content.contains("00:00:00.000 --> 00:00:05.000"));
696 }
697
698 #[test]
699 fn test_webvtt_roundtrip_basic() {
700 let format = WebVttFormat::new();
701 let options = FormatOptions::default();
702
703 let simple_vtt = "WEBVTT\n\n1\n00:00:01.000 --> 00:00:03.000\nHello World\n\n";
704
705 let (document1, _) = format.import_from_string(simple_vtt, &options).unwrap();
707 let (exported_content, _) = format.export_to_string(&document1, &options).unwrap();
708
709 assert!(exported_content.contains("WEBVTT"));
711 assert!(exported_content.contains("Hello World"));
712 assert!(exported_content.contains("00:00:01.000 --> 00:00:03.000"));
713 }
714
715 #[test]
716 fn test_webvtt_style_preservation() {
717 let format = WebVttFormat::new();
718 let options = FormatOptions::default();
719
720 let styled_vtt =
721 "WEBVTT\n\n1\n00:00:00.000 --> 00:00:02.000\n<b>Bold</b> and <i>italic</i> text\n\n";
722
723 let (document, _) = format.import_from_string(styled_vtt, &options).unwrap();
724 let (exported_content, _) = format.export_to_string(&document, &options).unwrap();
725
726 assert!(exported_content.contains("<b>Bold</b>"));
728 assert!(exported_content.contains("<i>italic</i>"));
729 }
730
731 #[test]
732 fn test_webvtt_positioning_support() {
733 let format = WebVttFormat::new();
734 let options = FormatOptions::default();
735
736 let positioned_vtt =
737 "WEBVTT\n\n1\n00:00:00.000 --> 00:00:02.000 line:20% position:50%\nPositioned text\n\n";
738
739 let (document, _) = format.import_from_string(positioned_vtt, &options).unwrap();
740
741 assert!(document.text().contains("Positioned text"));
743 }
744
745 #[test]
746 fn test_webvtt_multiline_handling() {
747 let format = WebVttFormat::new();
748 let options = FormatOptions::default();
749
750 let multiline_vtt =
751 "WEBVTT\n\n1\n00:00:00.000 --> 00:00:02.000\nLine one\nLine two\nLine three\n\n";
752
753 let (document, _) = format.import_from_string(multiline_vtt, &options).unwrap();
754 let (exported_content, _) = format.export_to_string(&document, &options).unwrap();
755
756 assert!(exported_content.contains("Line one"));
758 assert!(exported_content.contains("Line two"));
759 assert!(exported_content.contains("Line three"));
760 }
761
762 #[test]
763 fn test_webvtt_error_handling() {
764 let format = WebVttFormat::new();
765 let options = FormatOptions::default();
766
767 let invalid_vtt = "Invalid WebVTT content";
768 let result = format.import_from_string(invalid_vtt, &options);
769
770 if let Ok((_, format_result)) = result {
772 assert!(!format_result.warnings.is_empty());
773 }
774 }
775
776 #[test]
777 fn test_webvtt_metadata_extraction() {
778 let format = WebVttFormat::new();
779 let options = FormatOptions::default();
780
781 let (_, format_result) = format.import_from_string(SAMPLE_WEBVTT, &options).unwrap();
782
783 assert_eq!(
784 format_result.metadata.get("original_format"),
785 Some(&"WebVTT".to_string())
786 );
787 assert_eq!(
788 format_result.metadata.get("cues_count"),
789 Some(&"3".to_string())
790 );
791 assert_eq!(
792 format_result.metadata.get("encoding"),
793 Some(&"UTF-8".to_string())
794 );
795 }
796}