ass_core/parser/streaming/
processor.rs

1//! Line processing logic for streaming ASS parser
2//!
3//! Handles incremental processing of individual lines during streaming parsing,
4//! with context-aware processing based on current parser state.
5
6use crate::Result;
7use alloc::string::ToString;
8
9use super::{
10    delta::DeltaBatch,
11    state::{ParserState, SectionKind, StreamingContext},
12};
13
14/// Line processor for streaming ASS parser
15///
16/// Handles context-aware processing of individual lines based on current
17/// parser state and section type. Maintains state transitions and generates
18/// appropriate parse deltas.
19pub struct LineProcessor {
20    /// Current parser state
21    pub state: ParserState,
22    /// Parsing context with line tracking
23    pub context: StreamingContext,
24}
25
26impl LineProcessor {
27    /// Create new line processor
28    #[must_use]
29    pub const fn new() -> Self {
30        Self {
31            state: ParserState::ExpectingSection,
32            context: StreamingContext::new(),
33        }
34    }
35
36    /// Process a single complete line
37    ///
38    /// Dispatches line processing based on current state and line content.
39    /// Updates internal state and returns any generated deltas.
40    ///
41    /// # Errors
42    ///
43    /// Returns an error if the line contains malformed section headers or
44    /// other unrecoverable syntax errors during processing.
45    pub fn process_line(&mut self, line: &str) -> Result<DeltaBatch<'static>> {
46        self.context.next_line();
47        let trimmed = line.trim();
48
49        // Skip empty lines and comments
50        if trimmed.is_empty() || trimmed.starts_with(';') || trimmed.starts_with("!:") {
51            return Ok(DeltaBatch::new());
52        }
53
54        // Handle section headers
55        if trimmed.starts_with('[') && trimmed.ends_with(']') {
56            return Ok(self.process_section_header(trimmed));
57        }
58
59        // Handle section content based on current state
60        match &self.state {
61            ParserState::ExpectingSection => {
62                // Content outside sections - ignore or warn
63                Ok(DeltaBatch::new())
64            }
65            ParserState::InSection(section_kind) => {
66                Ok(self.process_section_content(line, *section_kind))
67            }
68            ParserState::InEvent {
69                section,
70                fields_seen,
71            } => Ok(self.process_event_continuation(line, *section, *fields_seen)),
72        }
73    }
74
75    /// Process section header line
76    fn process_section_header(&mut self, line: &str) -> DeltaBatch<'static> {
77        let section_name = &line[1..line.len() - 1]; // Remove [ ]
78        let section_kind = SectionKind::from_header(section_name);
79
80        // Update state
81        self.state.enter_section(section_kind);
82        self.context.enter_section(section_kind);
83
84        // Reset format for sections that expect it
85        if section_kind.expects_format() {
86            match section_kind {
87                SectionKind::Events => self.context.events_format = None,
88                SectionKind::Styles => self.context.styles_format = None,
89                _ => {}
90            }
91        }
92
93        DeltaBatch::new()
94    }
95
96    /// Process content within a section
97    fn process_section_content(
98        &mut self,
99        line: &str,
100        section_kind: SectionKind,
101    ) -> DeltaBatch<'static> {
102        match section_kind {
103            SectionKind::ScriptInfo => Self::process_script_info_line(line),
104            SectionKind::Styles => self.process_styles_line(line),
105            SectionKind::Events => self.process_events_line(line),
106            SectionKind::Fonts | SectionKind::Graphics => Self::process_binary_line(line),
107            SectionKind::Unknown => {
108                // Log unknown section content but continue parsing
109                DeltaBatch::new()
110            }
111        }
112    }
113
114    /// Process line in Script Info section
115    fn process_script_info_line(line: &str) -> DeltaBatch<'static> {
116        let trimmed = line.trim();
117
118        if let Some(colon_pos) = trimmed.find(':') {
119            let _key = trimmed[..colon_pos].trim();
120            let _value = trimmed[colon_pos + 1..].trim();
121            // TODO: Handle script info fields
122        }
123
124        DeltaBatch::new()
125    }
126
127    /// Process line in Styles section
128    fn process_styles_line(&mut self, line: &str) -> DeltaBatch<'static> {
129        let trimmed = line.trim();
130
131        if let Some(format_str) = trimmed.strip_prefix("Format:") {
132            let format_str = format_str.trim().to_string();
133            self.context.set_styles_format(format_str);
134        } else if trimmed.starts_with("Style:") {
135            // Style definition detected - in full parser this would create AST node
136        }
137
138        DeltaBatch::new()
139    }
140
141    /// Process line in Events section
142    fn process_events_line(&mut self, line: &str) -> DeltaBatch<'static> {
143        let trimmed = line.trim();
144
145        if let Some(format_str) = trimmed.strip_prefix("Format:") {
146            let format_str = format_str.trim().to_string();
147            self.context.set_events_format(format_str);
148            return DeltaBatch::new();
149        }
150
151        if trimmed.starts_with("Dialogue:") || trimmed.starts_with("Comment:") {
152            // Begin event parsing
153            self.state.enter_event(SectionKind::Events);
154            // In full parser, this would parse the event fields
155        }
156
157        DeltaBatch::new()
158    }
159
160    /// Process line in binary sections (Fonts/Graphics)
161    fn process_binary_line(line: &str) -> DeltaBatch<'static> {
162        let trimmed = line.trim();
163
164        if trimmed.contains(':') {
165            // Font/graphic filename declaration
166        } else {
167            // UU-encoded data line
168        }
169
170        DeltaBatch::new()
171    }
172
173    /// Process continuation of an event
174    fn process_event_continuation(
175        &mut self,
176        line: &str,
177        section: SectionKind,
178        _fields_seen: usize,
179    ) -> DeltaBatch<'static> {
180        let trimmed = line.trim();
181
182        if !trimmed.is_empty() {
183            // Process event continuation data
184        }
185
186        // Return to section state
187        self.state = ParserState::InSection(section);
188        DeltaBatch::new()
189    }
190
191    /// Reset processor state for new parsing session
192    pub fn reset(&mut self) {
193        self.state = ParserState::ExpectingSection;
194        self.context.reset();
195    }
196}
197
198impl Default for LineProcessor {
199    fn default() -> Self {
200        Self::new()
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207    #[cfg(not(feature = "std"))]
208    #[test]
209    fn processor_creation() {
210        let processor = LineProcessor::new();
211        assert_eq!(processor.context.line_number, 0);
212        assert!(!processor.state.is_in_section());
213    }
214
215    #[test]
216    fn section_header_processing() {
217        let mut processor = LineProcessor::new();
218        let result = processor.process_line("[Script Info]").unwrap();
219        assert!(result.is_empty());
220        assert!(processor.state.is_in_section());
221        assert_eq!(
222            processor.state.current_section(),
223            Some(SectionKind::ScriptInfo)
224        );
225    }
226
227    #[test]
228    fn comment_line_skipping() {
229        let mut processor = LineProcessor::new();
230        let result = processor.process_line("; This is a comment").unwrap();
231        assert!(result.is_empty());
232        assert_eq!(processor.context.line_number, 1);
233    }
234
235    #[test]
236    fn format_line_processing() {
237        let mut processor = LineProcessor::new();
238        processor.state.enter_section(SectionKind::Events);
239        processor.context.enter_section(SectionKind::Events);
240
241        let result = processor
242            .process_line("Format: Layer, Start, End, Style, Text")
243            .unwrap();
244        assert!(result.is_empty());
245        assert!(processor.context.events_format.is_some());
246    }
247
248    #[test]
249    fn processor_reset() {
250        let mut processor = LineProcessor::new();
251        processor.state.enter_section(SectionKind::Events);
252        processor.context.next_line();
253
254        processor.reset();
255        assert!(!processor.state.is_in_section());
256        assert_eq!(processor.context.line_number, 0);
257    }
258
259    #[test]
260    fn processor_default() {
261        let processor = LineProcessor::default();
262        assert_eq!(processor.context.line_number, 0);
263        assert!(!processor.state.is_in_section());
264    }
265
266    #[test]
267    fn empty_line_processing() {
268        let mut processor = LineProcessor::new();
269        let result = processor.process_line("").unwrap();
270        assert!(result.is_empty());
271        assert_eq!(processor.context.line_number, 1);
272
273        let result = processor.process_line("   \t  ").unwrap();
274        assert!(result.is_empty());
275        assert_eq!(processor.context.line_number, 2);
276    }
277
278    #[test]
279    fn different_comment_formats() {
280        let mut processor = LineProcessor::new();
281
282        let result = processor.process_line("; Standard comment").unwrap();
283        assert!(result.is_empty());
284
285        let result = processor.process_line("!: Aegisub comment").unwrap();
286        assert!(result.is_empty());
287
288        assert_eq!(processor.context.line_number, 2);
289    }
290
291    #[test]
292    fn all_section_headers() {
293        let mut processor = LineProcessor::new();
294
295        let sections = [
296            "[Script Info]",
297            "[V4+ Styles]",
298            "[Events]",
299            "[Fonts]",
300            "[Graphics]",
301            "[Unknown Section]",
302        ];
303
304        for section in &sections {
305            let result = processor.process_line(section).unwrap();
306            assert!(result.is_empty());
307            assert!(processor.state.is_in_section());
308        }
309    }
310
311    #[test]
312    fn script_info_line_processing() {
313        let mut processor = LineProcessor::new();
314        processor.state.enter_section(SectionKind::ScriptInfo);
315
316        let result = processor.process_line("Title: Test Script").unwrap();
317        assert!(result.is_empty());
318
319        let result = processor.process_line("Author: Test Author").unwrap();
320        assert!(result.is_empty());
321
322        let result = processor.process_line("ScriptType: v4.00+").unwrap();
323        assert!(result.is_empty());
324
325        // Malformed line without colon
326        let result = processor.process_line("Malformed line").unwrap();
327        assert!(result.is_empty());
328    }
329
330    #[test]
331    fn styles_line_processing() {
332        let mut processor = LineProcessor::new();
333        processor.state.enter_section(SectionKind::Styles);
334
335        let result = processor
336            .process_line("Format: Name, Fontname, Fontsize")
337            .unwrap();
338        assert!(result.is_empty());
339        assert!(processor.context.styles_format.is_some());
340
341        let result = processor.process_line("Style: Default,Arial,20").unwrap();
342        assert!(result.is_empty());
343    }
344
345    #[test]
346    fn events_line_processing() {
347        let mut processor = LineProcessor::new();
348        processor.state.enter_section(SectionKind::Events);
349
350        let result = processor
351            .process_line("Format: Layer, Start, End, Style, Text")
352            .unwrap();
353        assert!(result.is_empty());
354        assert!(processor.context.events_format.is_some());
355
356        let result = processor
357            .process_line("Dialogue: 0,0:00:00.00,0:00:05.00,Default,Hello")
358            .unwrap();
359        assert!(result.is_empty());
360
361        let result = processor
362            .process_line("Comment: 0,0:00:05.00,0:00:10.00,Default,Note")
363            .unwrap();
364        assert!(result.is_empty());
365    }
366
367    #[test]
368    fn binary_line_processing() {
369        let mut processor = LineProcessor::new();
370
371        // Test Fonts section
372        processor.state.enter_section(SectionKind::Fonts);
373        let result = processor.process_line("fontname: Arial.ttf").unwrap();
374        assert!(result.is_empty());
375
376        let result = processor
377            .process_line("AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDD")
378            .unwrap();
379        assert!(result.is_empty());
380
381        // Test Graphics section
382        processor.state.enter_section(SectionKind::Graphics);
383        let result = processor.process_line("graphic: logo.png").unwrap();
384        assert!(result.is_empty());
385
386        let result = processor
387            .process_line("0123456789ABCDEF0123456789ABCDEF")
388            .unwrap();
389        assert!(result.is_empty());
390    }
391
392    #[test]
393    fn event_continuation_processing() {
394        let mut processor = LineProcessor::new();
395        processor.state.enter_event(SectionKind::Events);
396
397        let result = processor.process_line("  continuation data").unwrap();
398        assert!(result.is_empty());
399        // Should return to section state
400        assert!(processor.state.is_in_section());
401        assert_eq!(processor.state.current_section(), Some(SectionKind::Events));
402
403        // Test with empty continuation
404        processor.state.enter_event(SectionKind::Events);
405        let result = processor.process_line("").unwrap();
406        assert!(result.is_empty());
407    }
408
409    #[test]
410    fn content_outside_sections() {
411        let mut processor = LineProcessor::new();
412        // Start in ExpectingSection state
413        assert!(!processor.state.is_in_section());
414
415        let result = processor
416            .process_line("Random content outside sections")
417            .unwrap();
418        assert!(result.is_empty());
419        // Should still not be in a section
420        assert!(!processor.state.is_in_section());
421    }
422
423    #[test]
424    fn section_header_edge_cases() {
425        let mut processor = LineProcessor::new();
426
427        // Section header with spaces
428        let result = processor.process_line("[ Script Info ]").unwrap();
429        assert!(result.is_empty());
430        assert!(processor.state.is_in_section());
431
432        // Empty section header
433        let result = processor.process_line("[]").unwrap();
434        assert!(result.is_empty());
435
436        // Malformed section headers should not crash
437        let result = processor.process_line("[Unclosed section").unwrap();
438        assert!(result.is_empty());
439
440        let result = processor.process_line("Unclosed section]").unwrap();
441        assert!(result.is_empty());
442    }
443
444    #[test]
445    fn unknown_section_processing() {
446        let mut processor = LineProcessor::new();
447        processor.state.enter_section(SectionKind::Unknown);
448
449        let result = processor
450            .process_line("Any content in unknown section")
451            .unwrap();
452        assert!(result.is_empty());
453
454        let result = processor.process_line("Key: Value").unwrap();
455        assert!(result.is_empty());
456    }
457
458    #[test]
459    fn line_counter_increments() {
460        let mut processor = LineProcessor::new();
461        assert_eq!(processor.context.line_number, 0);
462
463        processor.process_line("Line 1").unwrap();
464        assert_eq!(processor.context.line_number, 1);
465
466        processor.process_line("Line 2").unwrap();
467        assert_eq!(processor.context.line_number, 2);
468
469        processor.process_line("").unwrap();
470        assert_eq!(processor.context.line_number, 3);
471    }
472
473    #[test]
474    fn format_context_updates() {
475        let mut processor = LineProcessor::new();
476
477        // Test styles format
478        processor.state.enter_section(SectionKind::Styles);
479        processor.context.enter_section(SectionKind::Styles);
480
481        assert!(processor.context.styles_format.is_none());
482        processor
483            .process_line("Format: Name, Fontname, Fontsize, Bold")
484            .unwrap();
485        assert!(processor.context.styles_format.is_some());
486
487        // Test events format
488        processor.state.enter_section(SectionKind::Events);
489        processor.context.enter_section(SectionKind::Events);
490
491        assert!(processor.context.events_format.is_none());
492        processor
493            .process_line("Format: Layer, Start, End, Style, Text")
494            .unwrap();
495        assert!(processor.context.events_format.is_some());
496    }
497
498    #[test]
499    fn complex_processing_sequence() {
500        let mut processor = LineProcessor::new();
501
502        // Process a complete mini-script
503        let lines = [
504            "[Script Info]",
505            "Title: Test",
506            "Author: Tester",
507            "",
508            "[V4+ Styles]",
509            "Format: Name, Fontname, Fontsize",
510            "Style: Default,Arial,20",
511            "",
512            "[Events]",
513            "Format: Layer, Start, End, Style, Text",
514            "Dialogue: 0,0:00:00.00,0:00:05.00,Default,Hello World",
515            "; End of script",
516        ];
517
518        for line in &lines {
519            let result = processor.process_line(line).unwrap();
520            assert!(result.is_empty());
521        }
522
523        assert_eq!(processor.context.line_number, lines.len());
524        assert!(processor.context.events_format.is_some());
525        assert!(processor.context.styles_format.is_some());
526    }
527
528    #[test]
529    fn whitespace_handling() {
530        let mut processor = LineProcessor::new();
531
532        // Test various whitespace scenarios
533        processor.process_line("   [Script Info]   ").unwrap();
534        assert!(processor.state.is_in_section());
535
536        processor.process_line("\t\tTitle: Test\t\t").unwrap();
537
538        processor
539            .process_line("   ; Comment with spaces   ")
540            .unwrap();
541
542        processor.process_line("\t\n").unwrap(); // Tab followed by what looks like newline
543    }
544}