ass_core/plugin/sections/
aegisub.rs

1//! Aegisub-specific section processors for ASS compatibility
2//!
3//! Implements section processors for Aegisub-specific sections that extend
4//! the standard ASS format. These processors handle project metadata and
5//! additional data storage used by the Aegisub subtitle editor.
6//!
7//! # Supported Sections
8//!
9//! - `[Aegisub Project]`: Project-specific metadata and settings
10//! - `[Aegisub Extradata]`: Additional data storage for extended functionality
11//!
12//! # Performance
13//!
14//! - Zero allocations for validation
15//! - O(n) processing where n = number of lines
16//! - Minimal memory footprint per processor
17
18use crate::plugin::{SectionProcessor, SectionResult};
19use alloc::string::String;
20
21/// Handler for Aegisub Project section
22///
23/// Processes `[Aegisub Project]` sections containing project-specific metadata
24/// such as active line tracking, scroll position, and editor state.
25pub struct AegisubProjectProcessor;
26
27impl SectionProcessor for AegisubProjectProcessor {
28    fn name(&self) -> &'static str {
29        "Aegisub Project"
30    }
31
32    fn process(&self, header: &str, lines: &[&str]) -> SectionResult {
33        if !header.eq_ignore_ascii_case("Aegisub Project") {
34            return SectionResult::Ignored;
35        }
36
37        // Validate Aegisub project format
38        for line in lines {
39            let line = line.trim();
40            if line.is_empty() || line.starts_with('!') {
41                continue;
42            }
43
44            // Aegisub project lines should be in key=value or key: value format
45            if !line.contains('=') && !line.contains(':') {
46                return SectionResult::Failed(String::from(
47                    "Invalid Aegisub project line format (expected key=value or key: value)",
48                ));
49            }
50        }
51
52        SectionResult::Processed
53    }
54
55    fn validate(&self, header: &str, lines: &[&str]) -> bool {
56        header.eq_ignore_ascii_case("Aegisub Project") && !lines.is_empty()
57    }
58}
59
60/// Handler for Aegisub Extradata section
61///
62/// Processes `[Aegisub Extradata]` sections containing additional data storage
63/// for extended functionality beyond standard ASS format.
64pub struct AegisubExtradataProcessor;
65
66impl SectionProcessor for AegisubExtradataProcessor {
67    fn name(&self) -> &'static str {
68        "Aegisub Extradata"
69    }
70
71    fn process(&self, header: &str, lines: &[&str]) -> SectionResult {
72        if !header.eq_ignore_ascii_case("Aegisub Extradata") {
73            return SectionResult::Ignored;
74        }
75
76        // Validate extradata format - typically binary data or key-value pairs
77        for line in lines {
78            let line = line.trim();
79            if line.is_empty() {
80                continue;
81            }
82
83            // Extradata can be various formats, so we're permissive
84            // Just ensure it's not completely malformed
85            if line.len() > 10000 {
86                return SectionResult::Failed(String::from(
87                    "Extradata line exceeds maximum length",
88                ));
89            }
90        }
91
92        SectionResult::Processed
93    }
94
95    fn validate(&self, header: &str, _lines: &[&str]) -> bool {
96        header.eq_ignore_ascii_case("Aegisub Extradata")
97    }
98}
99
100/// Create all Aegisub section processors
101///
102/// Returns a vector of boxed section processors for all Aegisub-specific sections.
103/// Useful for bulk registration with the extension registry.
104///
105/// # Example
106///
107/// ```rust
108/// use ass_core::plugin::{ExtensionRegistry, sections::aegisub::create_aegisub_processors};
109///
110/// let mut registry = ExtensionRegistry::new();
111/// for processor in create_aegisub_processors() {
112///     registry.register_section_processor(processor).unwrap();
113/// }
114/// ```
115#[must_use]
116pub fn create_aegisub_processors() -> alloc::vec::Vec<alloc::boxed::Box<dyn SectionProcessor>> {
117    alloc::vec![
118        alloc::boxed::Box::new(AegisubProjectProcessor),
119        alloc::boxed::Box::new(AegisubExtradataProcessor),
120    ]
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    #[cfg(not(feature = "std"))]
127    use alloc::vec;
128
129    #[test]
130    fn aegisub_project_processor_valid() {
131        let processor = AegisubProjectProcessor;
132        let lines = vec!["Active Line: 0", "Video Position: 0"];
133
134        assert_eq!(
135            processor.process("Aegisub Project", &lines),
136            SectionResult::Processed
137        );
138    }
139
140    #[test]
141    fn aegisub_project_processor_invalid_header() {
142        let processor = AegisubProjectProcessor;
143        let lines = vec!["Active Line: 0"];
144
145        assert_eq!(
146            processor.process("Wrong Header", &lines),
147            SectionResult::Ignored
148        );
149    }
150
151    #[test]
152    fn aegisub_project_processor_invalid_format() {
153        let processor = AegisubProjectProcessor;
154        let lines = vec!["Invalid line without equals"];
155
156        assert!(matches!(
157            processor.process("Aegisub Project", &lines),
158            SectionResult::Failed(_)
159        ));
160    }
161
162    #[test]
163    fn aegisub_extradata_processor_valid() {
164        let processor = AegisubExtradataProcessor;
165        let lines = vec!["Data: some_binary_data", "More: extra_info"];
166
167        assert_eq!(
168            processor.process("Aegisub Extradata", &lines),
169            SectionResult::Processed
170        );
171    }
172
173    #[test]
174    fn aegisub_extradata_processor_long_line() {
175        let processor = AegisubExtradataProcessor;
176        let long_line = "x".repeat(20000);
177        let lines = vec![long_line.as_str()];
178
179        assert!(matches!(
180            processor.process("Aegisub Extradata", &lines),
181            SectionResult::Failed(_)
182        ));
183    }
184
185    #[test]
186    fn processor_names_correct() {
187        assert_eq!(AegisubProjectProcessor.name(), "Aegisub Project");
188        assert_eq!(AegisubExtradataProcessor.name(), "Aegisub Extradata");
189    }
190
191    #[test]
192    fn create_aegisub_processors_returns_two() {
193        let processors = create_aegisub_processors();
194        assert_eq!(processors.len(), 2);
195    }
196
197    #[test]
198    fn case_insensitive_headers() {
199        let processor = AegisubProjectProcessor;
200        let lines = vec!["Active Line: 0"];
201
202        assert_eq!(
203            processor.process("aegisub project", &lines),
204            SectionResult::Processed
205        );
206        assert_eq!(
207            processor.process("AEGISUB PROJECT", &lines),
208            SectionResult::Processed
209        );
210    }
211}