Skip to main content

ass_core/parser/sections/events/
static_parser.rs

1//! Stateless event-line parsing for incremental re-parsing.
2//!
3//! Exposes [`EventsParser::parse_event_line`], a static entry point that parses
4//! a single event line against an explicit format specification without needing
5//! a live parser instance, returning [`ParseError`] on malformed input.
6
7use super::EventsParser;
8use crate::parser::{
9    ast::{Event, EventType, Span},
10    errors::ParseError,
11};
12use alloc::vec::Vec;
13
14impl<'a> EventsParser<'a> {
15    /// Parse a single event line (make existing internal method public)
16    ///
17    /// Parses a single event line using the provided format specification.
18    /// This method is exposed for incremental parsing support.
19    ///
20    /// # Arguments
21    ///
22    /// * `line` - The event line to parse (e.g., "Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Text")
23    /// * `format` - The format fields from the Format line
24    /// * `line_number` - The line number for error reporting
25    ///
26    /// # Returns
27    ///
28    /// Parsed Event or error if the line is malformed
29    ///
30    /// # Errors
31    ///
32    /// Returns [`ParseError::InvalidEventType`] if the line doesn't start with a valid event type
33    /// Returns [`ParseError::InsufficientFields`] if the line has fewer fields than expected by format
34    pub fn parse_event_line(
35        line: &'a str,
36        format: &[&'a str],
37        line_number: u32,
38    ) -> core::result::Result<Event<'a>, ParseError> {
39        // Determine event type
40        let (event_type, data) = if let Some(data) = line.strip_prefix("Dialogue:") {
41            (EventType::Dialogue, data)
42        } else if let Some(data) = line.strip_prefix("Comment:") {
43            (EventType::Comment, data)
44        } else if let Some(data) = line.strip_prefix("Picture:") {
45            (EventType::Picture, data)
46        } else if let Some(data) = line.strip_prefix("Sound:") {
47            (EventType::Sound, data)
48        } else if let Some(data) = line.strip_prefix("Movie:") {
49            (EventType::Movie, data)
50        } else if let Some(data) = line.strip_prefix("Command:") {
51            (EventType::Command, data)
52        } else {
53            return Err(ParseError::InvalidEventType {
54                line: line_number as usize,
55            });
56        };
57
58        // Parse event data
59        Self::parse_event_data_static(event_type, data.trim(), format, line_number)
60    }
61
62    /// Static helper to parse event data fields
63    fn parse_event_data_static(
64        event_type: EventType,
65        data: &'a str,
66        format: &[&'a str],
67        line_number: u32,
68    ) -> core::result::Result<Event<'a>, ParseError> {
69        let format = if format.is_empty() {
70            &[
71                "Layer", "Start", "End", "Style", "Name", "MarginL", "MarginR", "MarginV",
72                "Effect", "Text",
73            ]
74        } else {
75            format
76        };
77
78        // Check if Text field exists in format to determine splitting strategy
79        let has_text_field = format
80            .iter()
81            .any(|&field| field.eq_ignore_ascii_case("Text"));
82
83        // Use appropriate splitting strategy to handle commas correctly
84        let parts: Vec<&str> = if has_text_field {
85            // If Text field exists, limit splits to preserve commas in text
86            data.splitn(format.len(), ',').collect()
87        } else {
88            // If no Text field, split all commas and ignore extra fields
89            data.splitn(10, ',').collect()
90        };
91
92        if parts.len() < format.len() {
93            return Err(ParseError::InsufficientFields {
94                expected: format.len(),
95                found: parts.len(),
96                line: line_number as usize,
97            });
98        }
99
100        let get_field = |name: &str| -> &'a str {
101            format
102                .iter()
103                .position(|&field| field.eq_ignore_ascii_case(name))
104                .and_then(|idx| parts.get(idx))
105                .map_or("", |s| s.trim())
106        };
107
108        // Create span for the event (caller will need to adjust this)
109        let span = Span::new(0, 0, line_number, 1);
110
111        Ok(Event {
112            event_type,
113            layer: get_field("Layer"),
114            start: get_field("Start"),
115            end: get_field("End"),
116            style: get_field("Style"),
117            name: get_field("Name"),
118            margin_l: get_field("MarginL"),
119            margin_r: get_field("MarginR"),
120            margin_v: get_field("MarginV"),
121            margin_t: format
122                .iter()
123                .any(|&f| f.eq_ignore_ascii_case("MarginT"))
124                .then(|| get_field("MarginT")),
125            margin_b: format
126                .iter()
127                .any(|&f| f.eq_ignore_ascii_case("MarginB"))
128                .then(|| get_field("MarginB")),
129            effect: get_field("Effect"),
130            text: get_field("Text"),
131            span,
132        })
133    }
134}