1use crate::core::{EditorDocument, EditorError};
7use crate::formats::{
8 Format, FormatExporter, FormatImporter, FormatInfo, FormatOptions, FormatResult,
9};
10use ass_core::parser::Script;
11use std::io::{Read, Write};
12
13#[derive(Debug)]
15pub struct AssFormat {
16 info: FormatInfo,
17}
18
19impl AssFormat {
20 pub fn new() -> Self {
22 Self {
23 info: FormatInfo {
24 name: "ASS".to_string(),
25 extensions: vec!["ass".to_string()],
26 mime_type: "text/x-ass".to_string(),
27 description: "Advanced SubStation Alpha subtitle format".to_string(),
28 supports_styling: true,
29 supports_positioning: true,
30 },
31 }
32 }
33}
34
35impl Default for AssFormat {
36 fn default() -> Self {
37 Self::new()
38 }
39}
40
41impl FormatImporter for AssFormat {
42 fn format_info(&self) -> &FormatInfo {
43 &self.info
44 }
45
46 fn import_from_reader(
47 &self,
48 reader: &mut dyn Read,
49 _options: &FormatOptions,
50 ) -> Result<(EditorDocument, FormatResult), EditorError> {
51 let mut content = String::new();
53 reader
54 .read_to_string(&mut content)
55 .map_err(|e| EditorError::IoError(format!("Failed to read content: {e}")))?;
56
57 let script = Script::parse(&content)?;
59
60 let line_count = content.lines().count();
62
63 let document = EditorDocument::from_content(&content)?;
65
66 let mut result = FormatResult::success(line_count);
68
69 if let Some(ass_core::parser::ast::Section::ScriptInfo(script_info)) =
71 script.find_section(ass_core::parser::ast::SectionType::ScriptInfo)
72 {
73 if let Some(title) = script_info.get_field("Title") {
74 result = result.with_metadata("title".to_string(), title.to_string());
75 }
76 if let Some(script_type) = script_info.get_field("ScriptType") {
77 result = result.with_metadata("script_type".to_string(), script_type.to_string());
78 }
79 }
80
81 let section_count = script.sections().len();
83
84 result = result.with_metadata("sections".to_string(), section_count.to_string());
85
86 Ok((document, result))
87 }
88}
89
90impl FormatExporter for AssFormat {
91 fn format_info(&self) -> &FormatInfo {
92 &self.info
93 }
94
95 fn export_to_writer(
96 &self,
97 document: &EditorDocument,
98 writer: &mut dyn Write,
99 options: &FormatOptions,
100 ) -> Result<FormatResult, EditorError> {
101 let content = if options.preserve_formatting {
102 document.text()
104 } else {
105 document.parse_script_with(|script| script.to_ass_string())?
107 };
108
109 let line_count = content.lines().count();
111
112 let bytes = if options.encoding.eq_ignore_ascii_case("UTF-8") {
114 content.into_bytes()
115 } else {
116 let mut warnings = Vec::new();
119 if !options.encoding.eq_ignore_ascii_case("UTF-8") {
120 warnings.push(format!(
121 "Encoding '{}' not supported, using UTF-8 instead",
122 options.encoding
123 ));
124 }
125
126 let result = FormatResult::success(line_count).with_warnings(warnings);
127
128 writer
129 .write_all(&content.into_bytes())
130 .map_err(|e| EditorError::IoError(format!("Failed to write content: {e}")))?;
131
132 return Ok(result);
133 };
134
135 writer
136 .write_all(&bytes)
137 .map_err(|e| EditorError::IoError(format!("Failed to write content: {e}")))?;
138
139 Ok(FormatResult::success(line_count))
140 }
141}
142
143impl Format for AssFormat {
144 fn as_importer(&self) -> &dyn FormatImporter {
145 self
146 }
147
148 fn as_exporter(&self) -> &dyn FormatExporter {
149 self
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156 #[cfg(not(feature = "std"))]
157 use alloc::string::ToString;
158 #[cfg(not(feature = "std"))]
159 use alloc::{format, string::String, vec};
160 use std::io::Cursor;
161
162 const SAMPLE_ASS: &str = r#"[Script Info]
163Title: Test Script
164ScriptType: v4.00+
165
166[V4+ Styles]
167Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
168Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H80000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
169
170[Events]
171Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
172Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Hello World!
173"#;
174
175 #[test]
176 fn test_ass_format_creation() {
177 let format = AssFormat::new();
178 let info = FormatImporter::format_info(&format);
179 assert_eq!(info.name, "ASS");
180 assert!(info.supports_styling);
181 assert!(info.supports_positioning);
182 assert!(format.can_import("ass"));
183 assert!(format.can_export("ass"));
184 }
185
186 #[test]
187 fn test_ass_import_from_string() {
188 let format = AssFormat::new();
189 let options = FormatOptions::default();
190
191 let result = format.import_from_string(SAMPLE_ASS, &options);
192 assert!(result.is_ok());
193
194 let (document, format_result) = result.unwrap();
195 assert!(format_result.success);
196 assert!(format_result.lines_processed > 0);
197 assert!(document.text().contains("Hello World!"));
198 }
199
200 #[test]
201 fn test_ass_export_to_string() {
202 let format = AssFormat::new();
203 let options = FormatOptions::default();
204
205 let (document, _) = format.import_from_string(SAMPLE_ASS, &options).unwrap();
207
208 let result = format.export_to_string(&document, &options);
210 assert!(result.is_ok());
211
212 let (exported_content, format_result) = result.unwrap();
213 assert!(format_result.success);
214 assert!(exported_content.contains("Hello World!"));
215 assert!(exported_content.contains("[Script Info]"));
216 }
217
218 #[test]
219 fn test_ass_roundtrip() {
220 let format = AssFormat::new();
221 let options = FormatOptions::default();
222
223 let (document1, _) = format.import_from_string(SAMPLE_ASS, &options).unwrap();
225 let (exported_content, _) = format.export_to_string(&document1, &options).unwrap();
226 let (document2, _) = format
227 .import_from_string(&exported_content, &options)
228 .unwrap();
229
230 assert_eq!(document1.text().trim(), document2.text().trim());
232 }
233
234 #[test]
235 fn test_ass_import_with_reader() {
236 let format = AssFormat::new();
237 let options = FormatOptions::default();
238 let mut cursor = Cursor::new(SAMPLE_ASS.as_bytes());
239
240 let result = format.import_from_reader(&mut cursor, &options);
241 assert!(result.is_ok());
242
243 let (document, format_result) = result.unwrap();
244 assert!(format_result.success);
245 assert!(document.text().contains("Test Script"));
246 }
247
248 #[test]
249 fn test_ass_export_with_writer() {
250 let format = AssFormat::new();
251 let options = FormatOptions::default();
252
253 let (document, _) = format.import_from_string(SAMPLE_ASS, &options).unwrap();
254
255 let mut buffer = Vec::new();
256 let result = format.export_to_writer(&document, &mut buffer, &options);
257 assert!(result.is_ok());
258
259 let exported_content = String::from_utf8(buffer).unwrap();
260 assert!(exported_content.contains("Hello World!"));
261 }
262
263 #[test]
264 fn test_ass_export_preserve_formatting() {
265 let format = AssFormat::new();
266 let options = FormatOptions {
267 preserve_formatting: true,
268 ..FormatOptions::default()
269 };
270
271 let (document, _) = format.import_from_string(SAMPLE_ASS, &options).unwrap();
272 let (exported_content, _) = format.export_to_string(&document, &options).unwrap();
273
274 assert_eq!(exported_content.trim(), SAMPLE_ASS.trim());
276 }
277
278 #[test]
279 fn test_ass_export_normalized() {
280 let format = AssFormat::new();
281 let options = FormatOptions {
282 preserve_formatting: false,
283 ..FormatOptions::default()
284 };
285
286 let messy_ass = r#"[Script Info]
288
289Title: Test Script
290ScriptType: v4.00+
291
292
293[V4+ Styles]
294Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
295Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H80000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
296
297[Events]
298Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
299Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Hello World!
300"#;
301
302 let (document, _) = format.import_from_string(messy_ass, &options).unwrap();
303 let (exported_content, _) = format.export_to_string(&document, &options).unwrap();
304
305 assert!(exported_content.contains("[Script Info]\nTitle: Test Script"));
307 assert!(!exported_content.contains("\n\n\n")); assert!(exported_content.contains("Hello World!"));
309 }
310
311 #[test]
312 fn test_ass_metadata_extraction() {
313 let format = AssFormat::new();
314 let options = FormatOptions::default();
315
316 let (_, format_result) = format.import_from_string(SAMPLE_ASS, &options).unwrap();
317
318 assert_eq!(
319 format_result.metadata.get("title"),
320 Some(&"Test Script".to_string())
321 );
322 assert_eq!(
323 format_result.metadata.get("script_type"),
324 Some(&"v4.00+".to_string())
325 );
326 assert!(format_result.metadata.contains_key("sections"));
327 }
328
329 #[test]
330 fn test_ass_export_normalized_format_lines() {
331 let format = AssFormat::new();
332 let options = FormatOptions {
333 preserve_formatting: false,
334 ..FormatOptions::default()
335 };
336
337 let minimal_ass = r#"[Script Info]
339Title: Test Script
340
341[V4+ Styles]
342Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H80000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
343
344[Events]
345Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Hello World!
346"#;
347
348 let (document, _) = format.import_from_string(minimal_ass, &options).unwrap();
349 let (exported_content, _) = format.export_to_string(&document, &options).unwrap();
350
351 assert!(exported_content.contains("[V4+ Styles]\n"));
354 assert!(exported_content.contains("Style: Default,Arial,20"));
355 assert!(exported_content.contains("[Events]\n"));
356 assert!(exported_content.contains("Dialogue: 0,0:00:00.00,0:00:05.00"));
357 }
358}