ass_editor/formats/
mod.rs

1//! Format import/export functionality for subtitle files.
2//!
3//! This module provides traits and implementations for importing and exporting
4//! various subtitle formats, reusing ass-core's parsing capabilities where possible.
5
6use crate::core::{EditorDocument, EditorError};
7use std::collections::HashMap;
8use std::fmt;
9use std::io::{Read, Write};
10use std::path::Path;
11
12/// Metadata about a subtitle format
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct FormatInfo {
15    /// Format name (e.g., "ASS", "SRT", "WebVTT")
16    pub name: String,
17    /// File extensions supported by this format
18    pub extensions: Vec<String>,
19    /// MIME type for this format
20    pub mime_type: String,
21    /// Brief description of the format
22    pub description: String,
23    /// Whether this format supports styling
24    pub supports_styling: bool,
25    /// Whether this format supports positioning
26    pub supports_positioning: bool,
27}
28
29/// Configuration options for format import/export operations
30#[derive(Debug, Clone)]
31pub struct FormatOptions {
32    /// Encoding to use (defaults to UTF-8)
33    pub encoding: String,
34    /// Whether to preserve formatting when possible
35    pub preserve_formatting: bool,
36    /// Custom options specific to each format
37    pub custom_options: HashMap<String, String>,
38}
39
40impl Default for FormatOptions {
41    fn default() -> Self {
42        Self {
43            encoding: "UTF-8".to_string(),
44            preserve_formatting: true,
45            custom_options: HashMap::new(),
46        }
47    }
48}
49
50/// Result of an import/export operation
51#[derive(Debug)]
52pub struct FormatResult {
53    /// Whether the operation succeeded
54    pub success: bool,
55    /// Number of lines/entries processed
56    pub lines_processed: usize,
57    /// Warnings encountered during processing
58    pub warnings: Vec<String>,
59    /// Additional metadata from the operation
60    pub metadata: HashMap<String, String>,
61}
62
63impl FormatResult {
64    pub fn success(lines_processed: usize) -> Self {
65        Self {
66            success: true,
67            lines_processed,
68            warnings: Vec::new(),
69            metadata: HashMap::new(),
70        }
71    }
72
73    pub fn with_warnings(mut self, warnings: Vec<String>) -> Self {
74        self.warnings = warnings;
75        self
76    }
77
78    pub fn with_metadata(mut self, key: String, value: String) -> Self {
79        self.metadata.insert(key, value);
80        self
81    }
82}
83
84/// Trait for importing subtitle files into EditorDocument
85pub trait FormatImporter: fmt::Debug + Send + Sync {
86    /// Get information about this format
87    fn format_info(&self) -> &FormatInfo;
88
89    /// Check if this importer can handle the given file extension
90    fn can_import(&self, extension: &str) -> bool {
91        self.format_info()
92            .extensions
93            .iter()
94            .any(|ext| ext.eq_ignore_ascii_case(extension))
95    }
96
97    /// Import from a reader with the given options
98    fn import_from_reader(
99        &self,
100        reader: &mut dyn Read,
101        options: &FormatOptions,
102    ) -> Result<(EditorDocument, FormatResult), EditorError>;
103
104    /// Import from a file path
105    fn import_from_path(
106        &self,
107        path: &Path,
108        options: &FormatOptions,
109    ) -> Result<(EditorDocument, FormatResult), EditorError> {
110        let mut file = std::fs::File::open(path)
111            .map_err(|e| EditorError::IoError(format!("Failed to open file: {e}")))?;
112        self.import_from_reader(&mut file, options)
113    }
114
115    /// Import from a string
116    fn import_from_string(
117        &self,
118        content: &str,
119        options: &FormatOptions,
120    ) -> Result<(EditorDocument, FormatResult), EditorError> {
121        let mut cursor = std::io::Cursor::new(content.as_bytes());
122        self.import_from_reader(&mut cursor, options)
123    }
124}
125
126/// Trait for exporting EditorDocument to subtitle files
127pub trait FormatExporter: fmt::Debug + Send + Sync {
128    /// Get information about this format
129    fn format_info(&self) -> &FormatInfo;
130
131    /// Check if this exporter can handle the given file extension
132    fn can_export(&self, extension: &str) -> bool {
133        self.format_info()
134            .extensions
135            .iter()
136            .any(|ext| ext.eq_ignore_ascii_case(extension))
137    }
138
139    /// Export to a writer with the given options
140    fn export_to_writer(
141        &self,
142        document: &EditorDocument,
143        writer: &mut dyn Write,
144        options: &FormatOptions,
145    ) -> Result<FormatResult, EditorError>;
146
147    /// Export to a file path
148    fn export_to_path(
149        &self,
150        document: &EditorDocument,
151        path: &Path,
152        options: &FormatOptions,
153    ) -> Result<FormatResult, EditorError> {
154        let mut file = std::fs::File::create(path)
155            .map_err(|e| EditorError::IoError(format!("Failed to create file: {e}")))?;
156        self.export_to_writer(document, &mut file, options)
157    }
158
159    /// Export to a string
160    fn export_to_string(
161        &self,
162        document: &EditorDocument,
163        options: &FormatOptions,
164    ) -> Result<(String, FormatResult), EditorError> {
165        let mut buffer = Vec::new();
166        let result = self.export_to_writer(document, &mut buffer, options)?;
167        let content = String::from_utf8(buffer)
168            .map_err(|e| EditorError::InvalidFormat(format!("Invalid UTF-8 output: {e}")))?;
169        Ok((content, result))
170    }
171}
172
173/// Combined trait for formats that support both import and export
174pub trait Format: FormatImporter + FormatExporter {
175    /// Get the format name
176    fn name(&self) -> &str {
177        &FormatImporter::format_info(self).name
178    }
179
180    /// Check if this format supports the given file extension
181    fn supports_extension(&self, extension: &str) -> bool {
182        self.can_import(extension) || self.can_export(extension)
183    }
184
185    /// Get self as an importer (workaround for trait upcasting)
186    fn as_importer(&self) -> &dyn FormatImporter;
187
188    /// Get self as an exporter (workaround for trait upcasting)
189    fn as_exporter(&self) -> &dyn FormatExporter;
190}
191
192/// Registry for managing available formats
193#[derive(Debug, Default)]
194pub struct FormatRegistry {
195    importers: HashMap<String, Box<dyn FormatImporter>>,
196    exporters: HashMap<String, Box<dyn FormatExporter>>,
197    formats: HashMap<String, Box<dyn Format>>,
198}
199
200impl FormatRegistry {
201    /// Create a new format registry
202    pub fn new() -> Self {
203        Self::default()
204    }
205
206    /// Register a format that supports both import and export
207    pub fn register_format(&mut self, format: Box<dyn Format>) {
208        let name = format.name().to_string();
209        self.formats.insert(name, format);
210    }
211
212    /// Register an importer
213    pub fn register_importer(&mut self, importer: Box<dyn FormatImporter>) {
214        let name = importer.format_info().name.clone();
215        self.importers.insert(name, importer);
216    }
217
218    /// Register an exporter
219    pub fn register_exporter(&mut self, exporter: Box<dyn FormatExporter>) {
220        let name = exporter.format_info().name.clone();
221        self.exporters.insert(name, exporter);
222    }
223
224    /// Find an importer for the given file extension
225    pub fn find_importer(&self, extension: &str) -> Option<&dyn FormatImporter> {
226        // Check combined formats first
227        for format in self.formats.values() {
228            if format.can_import(extension) {
229                return Some(format.as_importer());
230            }
231        }
232
233        // Check dedicated importers
234        for importer in self.importers.values() {
235            if importer.can_import(extension) {
236                return Some(importer.as_ref());
237            }
238        }
239
240        None
241    }
242
243    /// Find an exporter for the given file extension
244    pub fn find_exporter(&self, extension: &str) -> Option<&dyn FormatExporter> {
245        // Check combined formats first
246        for format in self.formats.values() {
247            if format.can_export(extension) {
248                return Some(format.as_exporter());
249            }
250        }
251
252        // Check dedicated exporters
253        for exporter in self.exporters.values() {
254            if exporter.can_export(extension) {
255                return Some(exporter.as_ref());
256            }
257        }
258
259        None
260    }
261
262    /// Get all supported import extensions
263    pub fn supported_import_extensions(&self) -> Vec<String> {
264        let mut extensions = Vec::new();
265
266        for format in self.formats.values() {
267            extensions.extend(
268                FormatImporter::format_info(format.as_ref())
269                    .extensions
270                    .clone(),
271            );
272        }
273
274        for importer in self.importers.values() {
275            extensions.extend(importer.format_info().extensions.clone());
276        }
277
278        extensions.sort();
279        extensions.dedup();
280        extensions
281    }
282
283    /// Get all supported export extensions
284    pub fn supported_export_extensions(&self) -> Vec<String> {
285        let mut extensions = Vec::new();
286
287        for format in self.formats.values() {
288            extensions.extend(
289                FormatExporter::format_info(format.as_ref())
290                    .extensions
291                    .clone(),
292            );
293        }
294
295        for exporter in self.exporters.values() {
296            extensions.extend(exporter.format_info().extensions.clone());
297        }
298
299        extensions.sort();
300        extensions.dedup();
301        extensions
302    }
303
304    /// Import a file using the appropriate format
305    pub fn import_file(
306        &self,
307        path: &Path,
308        options: Option<&FormatOptions>,
309    ) -> Result<(EditorDocument, FormatResult), EditorError> {
310        let extension = path
311            .extension()
312            .and_then(|ext| ext.to_str())
313            .ok_or_else(|| EditorError::InvalidFormat("No file extension found".to_string()))?;
314
315        let importer = self
316            .find_importer(extension)
317            .ok_or_else(|| EditorError::UnsupportedFormat(extension.to_string()))?;
318
319        let default_options = FormatOptions::default();
320        let options = options.unwrap_or(&default_options);
321        importer.import_from_path(path, options)
322    }
323
324    /// Export a document to a file using the appropriate format
325    pub fn export_file(
326        &self,
327        document: &EditorDocument,
328        path: &Path,
329        options: Option<&FormatOptions>,
330    ) -> Result<FormatResult, EditorError> {
331        let extension = path
332            .extension()
333            .and_then(|ext| ext.to_str())
334            .ok_or_else(|| EditorError::InvalidFormat("No file extension found".to_string()))?;
335
336        let exporter = self
337            .find_exporter(extension)
338            .ok_or_else(|| EditorError::UnsupportedFormat(extension.to_string()))?;
339
340        let default_options = FormatOptions::default();
341        let options = options.unwrap_or(&default_options);
342        exporter.export_to_path(document, path, options)
343    }
344}
345
346// Individual format modules
347pub mod ass;
348pub mod srt;
349pub mod webvtt;
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354    #[cfg(not(feature = "std"))]
355    use alloc::string::ToString;
356    #[cfg(not(feature = "std"))]
357    use alloc::{format, string::String, vec};
358
359    #[test]
360    fn test_format_info_creation() {
361        let info = FormatInfo {
362            name: "Test Format".to_string(),
363            extensions: vec!["test".to_string(), "tst".to_string()],
364            mime_type: "text/test".to_string(),
365            description: "A test format".to_string(),
366            supports_styling: true,
367            supports_positioning: false,
368        };
369
370        assert_eq!(info.name, "Test Format");
371        assert_eq!(info.extensions.len(), 2);
372        assert!(info.supports_styling);
373        assert!(!info.supports_positioning);
374    }
375
376    #[test]
377    fn test_format_options_default() {
378        let options = FormatOptions::default();
379        assert_eq!(options.encoding, "UTF-8");
380        assert!(options.preserve_formatting);
381        assert!(options.custom_options.is_empty());
382    }
383
384    #[test]
385    fn test_format_result_creation() {
386        let result = FormatResult::success(42)
387            .with_warnings(vec!["Warning 1".to_string()])
388            .with_metadata("key".to_string(), "value".to_string());
389
390        assert!(result.success);
391        assert_eq!(result.lines_processed, 42);
392        assert_eq!(result.warnings.len(), 1);
393        assert_eq!(result.metadata.get("key"), Some(&"value".to_string()));
394    }
395
396    #[test]
397    fn test_format_registry_creation() {
398        let registry = FormatRegistry::new();
399        assert!(registry.formats.is_empty());
400        assert!(registry.importers.is_empty());
401        assert!(registry.exporters.is_empty());
402    }
403
404    #[test]
405    fn test_format_registry_extensions() {
406        let registry = FormatRegistry::new();
407        let import_exts = registry.supported_import_extensions();
408        let export_exts = registry.supported_export_extensions();
409
410        assert!(import_exts.is_empty());
411        assert!(export_exts.is_empty());
412    }
413}