Skip to main content

ass_editor/formats/webvtt/
exporter.rs

1//! WebVTT export: serialise an `EditorDocument` to `.vtt` content.
2//!
3//! Implements [`FormatExporter`] for [`WebVttFormat`], converting ASS dialogue
4//! events into WebVTT cues with styling preserved.
5
6use crate::core::{EditorDocument, EditorError};
7use crate::formats::{FormatExporter, FormatInfo, FormatOptions, FormatResult};
8use std::io::Write;
9
10use super::WebVttFormat;
11
12impl FormatExporter for WebVttFormat {
13    fn format_info(&self) -> &FormatInfo {
14        &self.info
15    }
16
17    fn export_to_writer(
18        &self,
19        document: &EditorDocument,
20        writer: &mut dyn Write,
21        options: &FormatOptions,
22    ) -> Result<FormatResult, EditorError> {
23        // Parse the ASS content to extract events
24        let events = document.parse_script_with(|script| {
25            // Find events section and collect owned data
26            if let Some(ass_core::parser::ast::Section::Events(events)) =
27                script.find_section(ass_core::parser::ast::SectionType::Events)
28            {
29                // Convert to owned data to avoid lifetime issues
30                events
31                    .iter()
32                    .map(|event| {
33                        (
34                            event.event_type,
35                            event.start.to_string(),
36                            event.end.to_string(),
37                            event.text.to_string(),
38                        )
39                    })
40                    .collect::<Vec<_>>()
41            } else {
42                Vec::new()
43            }
44        })?;
45
46        let mut vtt_content = String::new();
47        let mut cue_num = 1;
48        let mut warnings = Vec::new();
49
50        // Add WebVTT header
51        vtt_content.push_str("WEBVTT\n\n");
52
53        for (event_type, start, end, text) in &events {
54            // Only export dialogue events
55            if event_type.as_str() != "Dialogue" {
56                continue;
57            }
58
59            // Parse start and end times
60            let start_time = match Self::format_vtt_time(start) {
61                Ok(time) => time,
62                Err(e) => {
63                    warnings.push(format!("Invalid start time for cue {cue_num}: {e}"));
64                    continue;
65                }
66            };
67
68            let end_time = match Self::format_vtt_time(end) {
69                Ok(time) => time,
70                Err(e) => {
71                    warnings.push(format!("Invalid end time for cue {cue_num}: {e}"));
72                    continue;
73                }
74            };
75
76            // Convert ASS text to WebVTT format
77            let mut text = text.clone();
78
79            // Convert ASS line breaks to actual line breaks
80            text = text.replace("\\N", "\n");
81            text = text.replace("\\n", "\n");
82
83            // Convert ASS styling to WebVTT styling
84            text = Self::convert_ass_to_vtt_styling(&text);
85
86            // Write WebVTT cue
87            vtt_content.push_str(&format!("{cue_num}\n"));
88            vtt_content.push_str(&format!("{start_time} --> {end_time}\n"));
89            vtt_content.push_str(&text);
90            vtt_content.push_str("\n\n");
91
92            cue_num += 1;
93        }
94
95        // Write content with proper encoding
96        let bytes = if options.encoding.eq_ignore_ascii_case("UTF-8") {
97            vtt_content.into_bytes()
98        } else {
99            warnings.push(format!(
100                "Encoding '{}' not supported, using UTF-8 instead",
101                options.encoding
102            ));
103            vtt_content.into_bytes()
104        };
105
106        writer
107            .write_all(&bytes)
108            .map_err(|e| EditorError::IoError(format!("Failed to write WebVTT content: {e}")))?;
109
110        let mut result = FormatResult::success(cue_num - 1)
111            .with_metadata("exported_format".to_string(), "WebVTT".to_string())
112            .with_metadata("cues_exported".to_string(), (cue_num - 1).to_string());
113
114        if !warnings.is_empty() {
115            result = result.with_warnings(warnings);
116        }
117
118        Ok(result)
119    }
120}