ass_core/parser/streaming/
state.rs

1//! State management for streaming ASS parser
2//!
3//! Provides state machine components for incremental parsing with
4//! proper section tracking and context management.
5
6use alloc::string::String;
7
8/// Streaming parser state for incremental processing
9///
10/// Tracks current parsing context to handle partial data and
11/// section boundaries correctly during streaming.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum ParserState {
14    /// Expecting section header or document start
15    ExpectingSection,
16    /// Currently parsing a specific section
17    InSection(SectionKind),
18    /// Parsing an event with potentially incomplete data
19    InEvent {
20        /// Which section type we're in
21        section: SectionKind,
22        /// Number of fields processed so far
23        fields_seen: usize,
24    },
25}
26
27impl ParserState {
28    /// Check if currently inside a section
29    #[must_use]
30    pub const fn is_in_section(&self) -> bool {
31        matches!(self, Self::InSection(_) | Self::InEvent { .. })
32    }
33
34    /// Get current section kind if in a section
35    #[must_use]
36    pub const fn current_section(&self) -> Option<SectionKind> {
37        match self {
38            Self::ExpectingSection => None,
39            Self::InSection(kind) => Some(*kind),
40            Self::InEvent { section, .. } => Some(*section),
41        }
42    }
43
44    /// Transition to new section
45    pub fn enter_section(&mut self, kind: SectionKind) {
46        *self = Self::InSection(kind);
47    }
48
49    /// Begin event parsing within section
50    pub fn enter_event(&mut self, section: SectionKind) {
51        *self = Self::InEvent {
52            section,
53            fields_seen: 0,
54        };
55    }
56
57    /// Exit current section
58    pub fn exit_section(&mut self) {
59        *self = Self::ExpectingSection;
60    }
61}
62
63/// Section types for state tracking
64///
65/// Identifies which ASS script section is currently being parsed
66/// to enable context-aware processing.
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum SectionKind {
69    /// [Script Info] section with metadata
70    ScriptInfo,
71    /// [V4+ Styles] or [V4 Styles] section
72    Styles,
73    /// `[Events\]` section with dialogue/timing
74    Events,
75    /// `[Fonts\]` section with embedded fonts
76    Fonts,
77    /// `[Graphics\]` section with embedded images
78    Graphics,
79    /// Unknown or unsupported section
80    Unknown,
81}
82
83impl SectionKind {
84    /// Parse section kind from header text
85    ///
86    /// Returns appropriate `SectionKind` for known section headers,
87    /// Unknown for unrecognized sections.
88    ///
89    /// # Example
90    ///
91    /// ```rust
92    /// # use ass_core::parser::streaming::SectionKind;
93    /// assert_eq!(SectionKind::from_header("Script Info"), SectionKind::ScriptInfo);
94    /// assert_eq!(SectionKind::from_header("V4+ Styles"), SectionKind::Styles);
95    /// assert_eq!(SectionKind::from_header("Unknown"), SectionKind::Unknown);
96    /// ```
97    #[must_use]
98    pub fn from_header(header: &str) -> Self {
99        match header.trim() {
100            "Script Info" => Self::ScriptInfo,
101            "V4+ Styles" | "V4 Styles" => Self::Styles,
102            "Events" => Self::Events,
103            "Fonts" => Self::Fonts,
104            "Graphics" => Self::Graphics,
105            _ => Self::Unknown,
106        }
107    }
108
109    /// Check if section expects format line
110    #[must_use]
111    pub const fn expects_format(&self) -> bool {
112        matches!(self, Self::Styles | Self::Events)
113    }
114
115    /// Check if section contains timed content
116    #[must_use]
117    pub const fn is_timed(&self) -> bool {
118        matches!(self, Self::Events)
119    }
120
121    /// Check if section contains binary data
122    #[must_use]
123    pub const fn is_binary(&self) -> bool {
124        matches!(self, Self::Fonts | Self::Graphics)
125    }
126}
127
128/// Context for streaming parser state
129///
130/// Maintains parsing context including line tracking, current section,
131/// and format information for proper incremental processing.
132#[derive(Debug, Clone)]
133pub struct StreamingContext {
134    /// Current line number (1-based)
135    pub line_number: usize,
136    /// Currently active section
137    pub current_section: Option<SectionKind>,
138    /// Events format fields
139    pub events_format: Option<String>,
140    /// Styles format fields
141    pub styles_format: Option<String>,
142}
143
144impl StreamingContext {
145    /// Create new context with default values
146    #[must_use]
147    pub const fn new() -> Self {
148        Self {
149            line_number: 0,
150            current_section: None,
151            events_format: None,
152            styles_format: None,
153        }
154    }
155
156    /// Advance to next line
157    pub fn next_line(&mut self) {
158        self.line_number += 1;
159    }
160
161    /// Enter new section
162    pub fn enter_section(&mut self, kind: SectionKind) {
163        self.current_section = Some(kind);
164    }
165
166    /// Exit current section
167    pub fn exit_section(&mut self) {
168        self.current_section = None;
169    }
170
171    /// Set format for events section
172    pub fn set_events_format(&mut self, format: String) {
173        self.events_format = Some(format);
174    }
175
176    /// Set format for styles section
177    pub fn set_styles_format(&mut self, format: String) {
178        self.styles_format = Some(format);
179    }
180
181    /// Reset context for new parsing session
182    pub fn reset(&mut self) {
183        self.line_number = 0;
184        self.current_section = None;
185        self.events_format = None;
186        self.styles_format = None;
187    }
188}
189
190impl Default for StreamingContext {
191    fn default() -> Self {
192        Self::new()
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199    #[cfg(not(feature = "std"))]
200    use alloc::{format, string::ToString};
201
202    #[test]
203    fn parser_state_transitions() {
204        let mut state = ParserState::ExpectingSection;
205        assert!(!state.is_in_section());
206        assert_eq!(state.current_section(), None);
207
208        state.enter_section(SectionKind::Events);
209        assert!(state.is_in_section());
210        assert_eq!(state.current_section(), Some(SectionKind::Events));
211
212        state.enter_event(SectionKind::Events);
213        assert!(state.is_in_section());
214        assert_eq!(state.current_section(), Some(SectionKind::Events));
215
216        state.exit_section();
217        assert!(!state.is_in_section());
218        assert_eq!(state.current_section(), None);
219    }
220
221    #[test]
222    fn section_kind_from_header() {
223        assert_eq!(
224            SectionKind::from_header("Script Info"),
225            SectionKind::ScriptInfo
226        );
227        assert_eq!(SectionKind::from_header("V4+ Styles"), SectionKind::Styles);
228        assert_eq!(SectionKind::from_header("V4 Styles"), SectionKind::Styles);
229        assert_eq!(SectionKind::from_header("Events"), SectionKind::Events);
230        assert_eq!(SectionKind::from_header("Fonts"), SectionKind::Fonts);
231        assert_eq!(SectionKind::from_header("Graphics"), SectionKind::Graphics);
232        assert_eq!(
233            SectionKind::from_header("Unknown Section"),
234            SectionKind::Unknown
235        );
236    }
237
238    #[test]
239    fn section_kind_properties() {
240        assert!(SectionKind::Styles.expects_format());
241        assert!(SectionKind::Events.expects_format());
242        assert!(!SectionKind::ScriptInfo.expects_format());
243
244        assert!(SectionKind::Events.is_timed());
245        assert!(!SectionKind::Styles.is_timed());
246
247        assert!(SectionKind::Fonts.is_binary());
248        assert!(SectionKind::Graphics.is_binary());
249        assert!(!SectionKind::Events.is_binary());
250    }
251
252    #[test]
253    fn streaming_context_operations() {
254        let mut context = StreamingContext::new();
255        assert_eq!(context.line_number, 0);
256        assert_eq!(context.current_section, None);
257
258        context.next_line();
259        assert_eq!(context.line_number, 1);
260
261        context.enter_section(SectionKind::Events);
262        assert_eq!(context.current_section, Some(SectionKind::Events));
263
264        context.set_events_format("Layer,Start,End,Text".to_string());
265        assert!(context.events_format.is_some());
266
267        context.reset();
268        assert_eq!(context.line_number, 0);
269        assert_eq!(context.current_section, None);
270        assert!(context.events_format.is_none());
271    }
272
273    #[test]
274    fn parser_state_debug_and_clone() {
275        let state = ParserState::ExpectingSection;
276        let debug_str = format!("{state:?}");
277        assert!(debug_str.contains("ExpectingSection"));
278
279        let cloned = state.clone();
280        assert_eq!(state, cloned);
281
282        let section_state = ParserState::InSection(SectionKind::Events);
283        let section_debug = format!("{section_state:?}");
284        assert!(section_debug.contains("InSection"));
285        assert!(section_debug.contains("Events"));
286
287        let event_state = ParserState::InEvent {
288            section: SectionKind::Events,
289            fields_seen: 3,
290        };
291        let event_debug = format!("{event_state:?}");
292        assert!(event_debug.contains("InEvent"));
293        assert!(event_debug.contains("fields_seen"));
294    }
295
296    #[test]
297    fn parser_state_equality() {
298        let state1 = ParserState::ExpectingSection;
299        let state2 = ParserState::ExpectingSection;
300        assert_eq!(state1, state2);
301
302        let state3 = ParserState::InSection(SectionKind::Events);
303        let state4 = ParserState::InSection(SectionKind::Events);
304        assert_eq!(state3, state4);
305
306        let state5 = ParserState::InEvent {
307            section: SectionKind::Events,
308            fields_seen: 2,
309        };
310        let state6 = ParserState::InEvent {
311            section: SectionKind::Events,
312            fields_seen: 2,
313        };
314        assert_eq!(state5, state6);
315
316        // Test inequality
317        assert_ne!(state1, state3);
318        assert_ne!(state3, state5);
319
320        let state7 = ParserState::InEvent {
321            section: SectionKind::Events,
322            fields_seen: 3,
323        };
324        assert_ne!(state5, state7);
325    }
326
327    #[test]
328    fn parser_state_all_variants() {
329        // Test ExpectingSection
330        let expecting = ParserState::ExpectingSection;
331        assert!(!expecting.is_in_section());
332        assert_eq!(expecting.current_section(), None);
333
334        // Test InSection for all section kinds
335        for &kind in &[
336            SectionKind::ScriptInfo,
337            SectionKind::Styles,
338            SectionKind::Events,
339            SectionKind::Fonts,
340            SectionKind::Graphics,
341            SectionKind::Unknown,
342        ] {
343            let in_section = ParserState::InSection(kind);
344            assert!(in_section.is_in_section());
345            assert_eq!(in_section.current_section(), Some(kind));
346        }
347
348        // Test InEvent
349        let in_event = ParserState::InEvent {
350            section: SectionKind::Events,
351            fields_seen: 5,
352        };
353        assert!(in_event.is_in_section());
354        assert_eq!(in_event.current_section(), Some(SectionKind::Events));
355    }
356
357    #[test]
358    fn section_kind_all_variants() {
359        let kinds = [
360            SectionKind::ScriptInfo,
361            SectionKind::Styles,
362            SectionKind::Events,
363            SectionKind::Fonts,
364            SectionKind::Graphics,
365            SectionKind::Unknown,
366        ];
367
368        for &kind in &kinds {
369            let debug_str = format!("{kind:?}");
370            assert!(!debug_str.is_empty());
371
372            // Test Copy trait
373            let copied = kind;
374            assert_eq!(kind, copied);
375        }
376    }
377
378    #[test]
379    fn section_kind_header_parsing_edge_cases() {
380        // Test case insensitive variations
381        assert_eq!(
382            SectionKind::from_header("  Script Info  "),
383            SectionKind::ScriptInfo
384        );
385        assert_eq!(
386            SectionKind::from_header("\tV4+ Styles\t"),
387            SectionKind::Styles
388        );
389
390        // Test empty and whitespace
391        assert_eq!(SectionKind::from_header(""), SectionKind::Unknown);
392        assert_eq!(SectionKind::from_header("   "), SectionKind::Unknown);
393
394        // Test partial matches
395        assert_eq!(SectionKind::from_header("Script"), SectionKind::Unknown);
396        assert_eq!(SectionKind::from_header("Info"), SectionKind::Unknown);
397        assert_eq!(SectionKind::from_header("Styles"), SectionKind::Unknown);
398
399        // Test common variations
400        assert_eq!(SectionKind::from_header("V4 Styles"), SectionKind::Styles);
401        assert_eq!(SectionKind::from_header("V4+ Styles"), SectionKind::Styles);
402    }
403
404    #[test]
405    fn section_kind_all_properties() {
406        // Test expects_format
407        assert!(SectionKind::Styles.expects_format());
408        assert!(SectionKind::Events.expects_format());
409        assert!(!SectionKind::ScriptInfo.expects_format());
410        assert!(!SectionKind::Fonts.expects_format());
411        assert!(!SectionKind::Graphics.expects_format());
412        assert!(!SectionKind::Unknown.expects_format());
413
414        // Test is_timed
415        assert!(SectionKind::Events.is_timed());
416        assert!(!SectionKind::ScriptInfo.is_timed());
417        assert!(!SectionKind::Styles.is_timed());
418        assert!(!SectionKind::Fonts.is_timed());
419        assert!(!SectionKind::Graphics.is_timed());
420        assert!(!SectionKind::Unknown.is_timed());
421
422        // Test is_binary
423        assert!(SectionKind::Fonts.is_binary());
424        assert!(SectionKind::Graphics.is_binary());
425        assert!(!SectionKind::ScriptInfo.is_binary());
426        assert!(!SectionKind::Styles.is_binary());
427        assert!(!SectionKind::Events.is_binary());
428        assert!(!SectionKind::Unknown.is_binary());
429    }
430
431    #[test]
432    fn streaming_context_default() {
433        let context = StreamingContext::default();
434        assert_eq!(context.line_number, 0);
435        assert_eq!(context.current_section, None);
436        assert!(context.events_format.is_none());
437        assert!(context.styles_format.is_none());
438    }
439
440    #[test]
441    fn streaming_context_debug_and_clone() {
442        let context = StreamingContext::new();
443        let debug_str = format!("{context:?}");
444        assert!(debug_str.contains("StreamingContext"));
445        assert!(debug_str.contains("line_number"));
446
447        let mut context_with_data = StreamingContext::new();
448        context_with_data.next_line();
449        context_with_data.enter_section(SectionKind::Events);
450        context_with_data.set_events_format("Test Format".to_string());
451
452        let cloned = context_with_data.clone();
453        assert_eq!(cloned.line_number, context_with_data.line_number);
454        assert_eq!(cloned.current_section, context_with_data.current_section);
455        assert_eq!(cloned.events_format, context_with_data.events_format);
456    }
457
458    #[test]
459    fn streaming_context_format_management() {
460        let mut context = StreamingContext::new();
461
462        // Test events format
463        assert!(context.events_format.is_none());
464        context.set_events_format("Layer, Start, End, Style, Text".to_string());
465        assert!(context.events_format.is_some());
466        assert_eq!(
467            context.events_format.as_ref().unwrap(),
468            "Layer, Start, End, Style, Text"
469        );
470
471        // Test styles format
472        assert!(context.styles_format.is_none());
473        context.set_styles_format("Name, Fontname, Fontsize".to_string());
474        assert!(context.styles_format.is_some());
475        assert_eq!(
476            context.styles_format.as_ref().unwrap(),
477            "Name, Fontname, Fontsize"
478        );
479
480        // Test reset clears formats
481        context.reset();
482        assert!(context.events_format.is_none());
483        assert!(context.styles_format.is_none());
484    }
485
486    #[test]
487    fn streaming_context_section_management() {
488        let mut context = StreamingContext::new();
489        assert_eq!(context.current_section, None);
490
491        context.enter_section(SectionKind::ScriptInfo);
492        assert_eq!(context.current_section, Some(SectionKind::ScriptInfo));
493
494        context.enter_section(SectionKind::Events);
495        assert_eq!(context.current_section, Some(SectionKind::Events));
496
497        context.exit_section();
498        assert_eq!(context.current_section, None);
499    }
500
501    #[test]
502    fn streaming_context_line_tracking() {
503        let mut context = StreamingContext::new();
504        assert_eq!(context.line_number, 0);
505
506        for expected_line in 1..=100 {
507            context.next_line();
508            assert_eq!(context.line_number, expected_line);
509        }
510
511        context.reset();
512        assert_eq!(context.line_number, 0);
513    }
514
515    #[test]
516    fn parser_state_transition_sequences() {
517        let mut state = ParserState::ExpectingSection;
518
519        // Test complete transition sequence
520        state.enter_section(SectionKind::Events);
521        assert!(state.is_in_section());
522        assert_eq!(state.current_section(), Some(SectionKind::Events));
523
524        state.enter_event(SectionKind::Events);
525        assert!(state.is_in_section());
526        assert_eq!(state.current_section(), Some(SectionKind::Events));
527
528        state.exit_section();
529        assert!(!state.is_in_section());
530        assert_eq!(state.current_section(), None);
531
532        // Test direct event entry
533        state.enter_event(SectionKind::Styles);
534        assert!(state.is_in_section());
535        assert_eq!(state.current_section(), Some(SectionKind::Styles));
536    }
537
538    #[test]
539    fn complex_state_context_interaction() {
540        let mut state = ParserState::ExpectingSection;
541        let mut context = StreamingContext::new();
542
543        // Simulate processing script
544        context.next_line(); // Line 1
545        state.enter_section(SectionKind::ScriptInfo);
546        context.enter_section(SectionKind::ScriptInfo);
547
548        context.next_line(); // Line 2
549        context.next_line(); // Line 3
550
551        state.enter_section(SectionKind::Events);
552        context.enter_section(SectionKind::Events);
553        context.set_events_format("Layer, Start, End, Text".to_string());
554
555        context.next_line(); // Line 4
556        state.enter_event(SectionKind::Events);
557
558        assert_eq!(context.line_number, 4);
559        assert!(context.events_format.is_some());
560        assert_eq!(context.current_section, Some(SectionKind::Events));
561        assert_eq!(state.current_section(), Some(SectionKind::Events));
562    }
563}