ass_core/parser/sections/
events.rs

1//! Events section parser for ASS scripts.
2//!
3//! Handles parsing of the `[Events]` section which contains dialogue, comments,
4//! and other timed events with format specifications and event entries.
5
6use crate::parser::{
7    ast::{Event, EventType, Section, Span},
8    errors::{IssueCategory, IssueSeverity, ParseError, ParseIssue},
9    position_tracker::PositionTracker,
10    sections::SectionParseResult,
11    ParseResult,
12};
13use alloc::{format, vec::Vec};
14
15/// Parser for `[Events]` section content
16///
17/// Parses format definitions and event entries from the events section.
18/// Uses format mapping to handle different field orderings and event types.
19///
20/// # Performance
21///
22/// - Time complexity: O(n * m) for n events and m fields per event
23/// - Memory: Zero allocations via lifetime-generic spans
24/// - Target: <2ms for typical event sections with 1000 events
25pub struct EventsParser<'a> {
26    /// Position tracker for accurate span generation
27    tracker: PositionTracker<'a>,
28    /// Parse issues and warnings collected during parsing
29    issues: Vec<ParseIssue>,
30    /// Format fields for the events section
31    format: Option<Vec<&'a str>>,
32}
33
34impl<'a> EventsParser<'a> {
35    /// Parse a single event line (make existing internal method public)
36    ///
37    /// Parses a single event line using the provided format specification.
38    /// This method is exposed for incremental parsing support.
39    ///
40    /// # Arguments
41    ///
42    /// * `line` - The event line to parse (e.g., "Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Text")
43    /// * `format` - The format fields from the Format line
44    /// * `line_number` - The line number for error reporting
45    ///
46    /// # Returns
47    ///
48    /// Parsed Event or error if the line is malformed
49    ///
50    /// # Errors
51    ///
52    /// Returns [`ParseError::InvalidEventType`] if the line doesn't start with a valid event type
53    /// Returns [`ParseError::InsufficientFields`] if the line has fewer fields than expected by format
54    pub fn parse_event_line(
55        line: &'a str,
56        format: &[&'a str],
57        line_number: u32,
58    ) -> core::result::Result<Event<'a>, ParseError> {
59        // Determine event type
60        let (event_type, data) = if let Some(data) = line.strip_prefix("Dialogue:") {
61            (EventType::Dialogue, data)
62        } else if let Some(data) = line.strip_prefix("Comment:") {
63            (EventType::Comment, data)
64        } else if let Some(data) = line.strip_prefix("Picture:") {
65            (EventType::Picture, data)
66        } else if let Some(data) = line.strip_prefix("Sound:") {
67            (EventType::Sound, data)
68        } else if let Some(data) = line.strip_prefix("Movie:") {
69            (EventType::Movie, data)
70        } else if let Some(data) = line.strip_prefix("Command:") {
71            (EventType::Command, data)
72        } else {
73            return Err(ParseError::InvalidEventType {
74                line: line_number as usize,
75            });
76        };
77
78        // Parse event data
79        Self::parse_event_data_static(event_type, data.trim(), format, line_number)
80    }
81
82    /// Static helper to parse event data fields
83    fn parse_event_data_static(
84        event_type: EventType,
85        data: &'a str,
86        format: &[&'a str],
87        line_number: u32,
88    ) -> core::result::Result<Event<'a>, ParseError> {
89        let format = if format.is_empty() {
90            &[
91                "Layer", "Start", "End", "Style", "Name", "MarginL", "MarginR", "MarginV",
92                "Effect", "Text",
93            ]
94        } else {
95            format
96        };
97
98        // Check if Text field exists in format to determine splitting strategy
99        let has_text_field = format
100            .iter()
101            .any(|&field| field.eq_ignore_ascii_case("Text"));
102
103        // Use appropriate splitting strategy to handle commas correctly
104        let parts: Vec<&str> = if has_text_field {
105            // If Text field exists, limit splits to preserve commas in text
106            data.splitn(format.len(), ',').collect()
107        } else {
108            // If no Text field, split all commas and ignore extra fields
109            data.splitn(10, ',').collect()
110        };
111
112        if parts.len() < format.len() {
113            return Err(ParseError::InsufficientFields {
114                expected: format.len(),
115                found: parts.len(),
116                line: line_number as usize,
117            });
118        }
119
120        let get_field = |name: &str| -> &'a str {
121            format
122                .iter()
123                .position(|&field| field.eq_ignore_ascii_case(name))
124                .and_then(|idx| parts.get(idx))
125                .map_or("", |s| s.trim())
126        };
127
128        // Create span for the event (caller will need to adjust this)
129        let span = Span::new(0, 0, line_number, 1);
130
131        Ok(Event {
132            event_type,
133            layer: get_field("Layer"),
134            start: get_field("Start"),
135            end: get_field("End"),
136            style: get_field("Style"),
137            name: get_field("Name"),
138            margin_l: get_field("MarginL"),
139            margin_r: get_field("MarginR"),
140            margin_v: get_field("MarginV"),
141            margin_t: format
142                .iter()
143                .any(|&f| f.eq_ignore_ascii_case("MarginT"))
144                .then(|| get_field("MarginT")),
145            margin_b: format
146                .iter()
147                .any(|&f| f.eq_ignore_ascii_case("MarginB"))
148                .then(|| get_field("MarginB")),
149            effect: get_field("Effect"),
150            text: get_field("Text"),
151            span,
152        })
153    }
154    /// Create new events parser for source text
155    ///
156    /// # Arguments
157    ///
158    /// * `source` - Source text to parse
159    /// * `start_position` - Starting byte position in source
160    /// * `start_line` - Starting line number for error reporting
161    #[must_use]
162    #[allow(clippy::missing_const_for_fn)] // Can't be const due to Vec::new()
163    pub fn new(source: &'a str, start_position: usize, start_line: usize) -> Self {
164        Self {
165            tracker: PositionTracker::new_at(
166                source,
167                start_position,
168                u32::try_from(start_line).unwrap_or(u32::MAX),
169                1,
170            ),
171            issues: Vec::new(),
172            format: None,
173        }
174    }
175
176    /// Create a new parser with a pre-known format for incremental parsing
177    #[must_use]
178    pub fn with_format(
179        source: &'a str,
180        format: &[&'a str],
181        start_position: usize,
182        start_line: u32,
183    ) -> Self {
184        Self {
185            tracker: PositionTracker::new_at(source, start_position, start_line, 1),
186            issues: Vec::new(),
187            format: Some(format.to_vec()),
188        }
189    }
190
191    /// Parse events section content
192    ///
193    /// Returns the parsed section and any issues encountered during parsing.
194    /// Handles Format line parsing and event entry validation.
195    ///
196    /// # Returns
197    ///
198    /// Tuple of (`parsed_section`, `format_fields`, `parse_issues`, `final_position`, `final_line`)
199    ///
200    /// # Errors
201    ///
202    /// Returns an error if the events section contains malformed format lines or
203    /// other unrecoverable syntax errors.
204    pub fn parse(mut self) -> ParseResult<SectionParseResult<'a>> {
205        let mut events = Vec::new();
206
207        while !self.tracker.is_at_end() && !self.at_next_section() {
208            self.skip_whitespace_and_comments();
209
210            if self.tracker.is_at_end() || self.at_next_section() {
211                break;
212            }
213
214            let line_start = self.tracker.checkpoint();
215            let line = self.current_line().trim();
216
217            if line.is_empty() {
218                self.tracker.skip_line();
219                continue;
220            }
221
222            if line.starts_with("Format:") {
223                self.parse_format_line(line);
224            } else if let Some(event) = self.parse_event_line_internal(line, &line_start) {
225                events.push(event);
226            }
227
228            self.tracker.skip_line();
229        }
230
231        Ok((
232            Section::Events(events),
233            self.format,
234            self.issues,
235            self.tracker.offset(),
236            self.tracker.line() as usize,
237        ))
238    }
239
240    /// Parse format specification line
241    fn parse_format_line(&mut self, line: &'a str) {
242        if let Some(format_data) = line.strip_prefix("Format:") {
243            let fields: Vec<&'a str> = format_data.split(',').map(str::trim).collect();
244            self.format = Some(fields);
245        }
246    }
247
248    /// Parse single event line (Dialogue, Comment, etc.)
249    fn parse_event_line_internal(
250        &mut self,
251        line: &'a str,
252        line_start: &PositionTracker<'a>,
253    ) -> Option<Event<'a>> {
254        let (event_type, data) = if let Some(data) = line.strip_prefix("Dialogue:") {
255            (EventType::Dialogue, data)
256        } else if let Some(data) = line.strip_prefix("Comment:") {
257            (EventType::Comment, data)
258        } else if let Some(data) = line.strip_prefix("Picture:") {
259            (EventType::Picture, data)
260        } else if let Some(data) = line.strip_prefix("Sound:") {
261            (EventType::Sound, data)
262        } else if let Some(data) = line.strip_prefix("Movie:") {
263            (EventType::Movie, data)
264        } else if let Some(data) = line.strip_prefix("Command:") {
265            (EventType::Command, data)
266        } else {
267            return None;
268        };
269
270        self.parse_event_data(event_type, data.trim(), line_start)
271    }
272
273    /// Parse event data fields using format mapping
274    fn parse_event_data(
275        &mut self,
276        event_type: EventType,
277        data: &'a str,
278        line_start: &PositionTracker<'a>,
279    ) -> Option<Event<'a>> {
280        let format = self.format.as_deref().unwrap_or(&[
281            "Layer", "Start", "End", "Style", "Name", "MarginL", "MarginR", "MarginV", "Effect",
282            "Text",
283        ]);
284
285        // Check if Text field exists in format to determine splitting strategy
286        let has_text_field = format
287            .iter()
288            .any(|&field| field.eq_ignore_ascii_case("Text"));
289
290        // Use appropriate splitting strategy to handle commas correctly
291        let parts: Vec<&str> = if has_text_field {
292            // If Text field exists, limit splits to preserve commas in text
293            data.splitn(format.len(), ',').collect()
294        } else {
295            // If no Text field, split all commas and ignore extra fields
296            data.splitn(10, ',').collect()
297        };
298
299        if parts.len() < format.len() {
300            self.issues.push(ParseIssue::new(
301                IssueSeverity::Warning,
302                IssueCategory::Format,
303                format!(
304                    "Event line has {} fields, expected at least {}",
305                    parts.len(),
306                    format.len()
307                ),
308                line_start.line() as usize,
309            ));
310            return None;
311        }
312
313        let get_field = |name: &str| -> &'a str {
314            format
315                .iter()
316                .position(|&field| field.eq_ignore_ascii_case(name))
317                .and_then(|idx| parts.get(idx))
318                .map_or("", |s| s.trim())
319        };
320
321        let text = get_field("Text");
322
323        // Calculate span for this event line
324        // We need to get the original line length from parse_event_line
325        // For now, use the current line method to get the full line
326        let full_line = self.current_line();
327        let span = line_start.span_for(full_line.len());
328
329        Some(Event {
330            event_type,
331            layer: get_field("Layer"),
332            start: get_field("Start"),
333            end: get_field("End"),
334            style: get_field("Style"),
335            name: get_field("Name"),
336            margin_l: get_field("MarginL"),
337            margin_r: get_field("MarginR"),
338            margin_v: get_field("MarginV"),
339            margin_t: format
340                .iter()
341                .any(|&f| f.eq_ignore_ascii_case("MarginT"))
342                .then(|| get_field("MarginT")),
343            margin_b: format
344                .iter()
345                .any(|&f| f.eq_ignore_ascii_case("MarginB"))
346                .then(|| get_field("MarginB")),
347            effect: get_field("Effect"),
348            text,
349            span,
350        })
351    }
352
353    /// Get current line from source
354    fn current_line(&self) -> &'a str {
355        let remaining = self.tracker.remaining();
356        let end = remaining.find('\n').unwrap_or(remaining.len());
357        &remaining[..end]
358    }
359
360    /// Check if at start of next section
361    #[must_use]
362    fn at_next_section(&self) -> bool {
363        self.tracker.remaining().trim_start().starts_with('[')
364    }
365
366    /// Skip whitespace and comment lines
367    fn skip_whitespace_and_comments(&mut self) {
368        loop {
369            self.tracker.skip_whitespace();
370
371            let remaining = self.tracker.remaining();
372            if remaining.is_empty() {
373                break;
374            }
375
376            if remaining.starts_with(';') || remaining.starts_with('#') {
377                self.tracker.skip_line();
378                continue;
379            }
380
381            // Check for newlines in whitespace
382            if remaining.starts_with('\n') {
383                self.tracker.advance(1);
384                continue;
385            }
386
387            break;
388        }
389    }
390
391    /// Get accumulated parse issues
392    #[must_use]
393    pub fn issues(self) -> Vec<ParseIssue> {
394        self.issues
395    }
396
397    /// Get format specification
398    #[must_use]
399    pub const fn format(&self) -> Option<&Vec<&'a str>> {
400        self.format.as_ref()
401    }
402}
403
404#[cfg(test)]
405mod tests {
406    use super::*;
407    #[cfg(not(feature = "std"))]
408    use alloc::vec;
409
410    #[test]
411    fn parse_empty_section() {
412        let parser = EventsParser::new("", 0, 1);
413        let result = parser.parse();
414        assert!(result.is_ok());
415
416        let (section, ..) = result.unwrap();
417        if let Section::Events(events) = section {
418            assert!(events.is_empty());
419        } else {
420            panic!("Expected Events section");
421        }
422    }
423
424    #[test]
425    fn parse_with_format_and_dialogue() {
426        let content = "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Hello World!\n";
427        let parser = EventsParser::new(content, 0, 1);
428        let result = parser.parse();
429        assert!(result.is_ok());
430
431        let (section, ..) = result.unwrap();
432        if let Section::Events(events) = section {
433            assert_eq!(events.len(), 1);
434            let event = &events[0];
435            assert!(matches!(event.event_type, EventType::Dialogue));
436            assert_eq!(event.start, "0:00:00.00");
437            assert_eq!(event.end, "0:00:05.00");
438            assert_eq!(event.style, "Default");
439            assert_eq!(event.text, "Hello World!");
440            // Check span
441            assert!(event.span.start > 0);
442            assert!(event.span.end > event.span.start);
443        } else {
444            panic!("Expected Events section");
445        }
446    }
447
448    #[test]
449    fn parse_different_event_types() {
450        let content = "Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Dialogue\nComment: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Comment\n";
451        let parser = EventsParser::new(content, 0, 1);
452        let result = parser.parse();
453        assert!(result.is_ok());
454
455        let (section, ..) = result.unwrap();
456        if let Section::Events(events) = section {
457            assert_eq!(events.len(), 2);
458            assert!(matches!(events[0].event_type, EventType::Dialogue));
459            assert!(matches!(events[1].event_type, EventType::Comment));
460            assert_eq!(events[0].text, "Dialogue");
461            assert_eq!(events[1].text, "Comment");
462        } else {
463            panic!("Expected Events section");
464        }
465    }
466
467    #[test]
468    fn handle_text_with_commas() {
469        let content =
470            "Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Hello, world, with commas!\n";
471        let parser = EventsParser::new(content, 0, 1);
472        let result = parser.parse();
473        assert!(result.is_ok());
474
475        let (section, ..) = result.unwrap();
476        if let Section::Events(events) = section {
477            assert_eq!(events.len(), 1);
478            assert_eq!(events[0].text, "Hello, world, with commas!");
479        } else {
480            panic!("Expected Events section");
481        }
482    }
483
484    #[test]
485    fn skip_comments_and_whitespace() {
486        let content = "; Comment\n# Another comment\n\nDialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Test\n";
487        let parser = EventsParser::new(content, 0, 1);
488        let result = parser.parse();
489        assert!(result.is_ok());
490
491        let (section, ..) = result.unwrap();
492        if let Section::Events(events) = section {
493            assert_eq!(events.len(), 1);
494            assert_eq!(events[0].text, "Test");
495        } else {
496            panic!("Expected Events section");
497        }
498    }
499
500    #[test]
501    fn parse_with_position_tracking() {
502        // Create a larger content that simulates a full file
503        let prefix = "a".repeat(200); // 200 bytes of padding
504        let section_content = "Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Test Event\n";
505        let full_content = format!("{prefix}{section_content}");
506
507        // Parser starts at position 200
508        let parser = EventsParser::new(&full_content, 200, 15);
509        let result = parser.parse();
510        assert!(result.is_ok());
511
512        let (section, _, _, final_pos, final_line) = result.unwrap();
513        if let Section::Events(events) = section {
514            assert_eq!(events.len(), 1);
515            let event = &events[0];
516            assert_eq!(event.span.start, 200);
517            assert_eq!(event.span.line, 15);
518            assert_eq!(event.text, "Test Event");
519        } else {
520            panic!("Expected Events section");
521        }
522
523        assert_eq!(final_pos, 200 + section_content.len());
524        assert_eq!(final_line, 16);
525    }
526
527    #[test]
528    fn parse_without_format_line() {
529        let content = "Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,No format line\n";
530        let parser = EventsParser::new(content, 0, 1);
531        let result = parser.parse();
532        assert!(result.is_ok());
533
534        let (section, format, ..) = result.unwrap();
535        if let Section::Events(events) = section {
536            assert_eq!(events.len(), 1);
537            assert_eq!(events[0].text, "No format line");
538        } else {
539            panic!("Expected Events section");
540        }
541        assert!(format.is_none());
542    }
543
544    #[test]
545    fn test_public_parse_event_line() {
546        let format = vec![
547            "Layer", "Start", "End", "Style", "Name", "MarginL", "MarginR", "MarginV", "Effect",
548            "Text",
549        ];
550        let line = "Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Test text";
551
552        let result = EventsParser::parse_event_line(line, &format, 1);
553        assert!(result.is_ok());
554
555        let event = result.unwrap();
556        assert!(matches!(event.event_type, EventType::Dialogue));
557        assert_eq!(event.start, "0:00:00.00");
558        assert_eq!(event.end, "0:00:05.00");
559        assert_eq!(event.style, "Default");
560        assert_eq!(event.text, "Test text");
561    }
562
563    #[test]
564    fn test_parse_event_line_invalid_type() {
565        let format = vec!["Layer", "Start", "End", "Style", "Text"];
566        let line = "Invalid: 0,0:00:00.00,0:00:05.00,Default,Test";
567
568        let result = EventsParser::parse_event_line(line, &format, 1);
569        assert!(result.is_err());
570
571        if let Err(e) = result {
572            assert!(matches!(e, ParseError::InvalidEventType { .. }));
573        }
574    }
575
576    #[test]
577    fn test_parse_event_line_insufficient_fields() {
578        let format = vec![
579            "Layer", "Start", "End", "Style", "Name", "MarginL", "MarginR", "MarginV", "Effect",
580            "Text",
581        ];
582        let line = "Dialogue: 0,0:00:00.00,0:00:05.00"; // Missing fields
583
584        let result = EventsParser::parse_event_line(line, &format, 1);
585        assert!(result.is_err());
586
587        if let Err(e) = result {
588            assert!(matches!(e, ParseError::InsufficientFields { .. }));
589        }
590    }
591
592    #[test]
593    fn test_parse_event_line_with_commas_in_text() {
594        let format = vec![
595            "Layer", "Start", "End", "Style", "Name", "MarginL", "MarginR", "MarginV", "Effect",
596            "Text",
597        ];
598        let line = "Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Hello, world, with commas!";
599
600        let result = EventsParser::parse_event_line(line, &format, 1);
601        assert!(result.is_ok());
602
603        let event = result.unwrap();
604        assert_eq!(event.text, "Hello, world, with commas!");
605    }
606}