ass-core 0.1.2

High-performance ASS subtitle format parser and analyzer
Documentation
//! Streaming parse lifecycle for the `[Events]` section.
//!
//! Implements [`EventsParser`] construction, the top-level parse loop, format
//! line handling, and the whitespace/comment control helpers used while
//! scanning event entries.

use super::EventsParser;
use crate::parser::{
    ast::Section, errors::ParseIssue, position_tracker::PositionTracker,
    sections::SectionParseResult, ParseResult,
};
use alloc::vec::Vec;

impl<'a> EventsParser<'a> {
    /// Create new events parser for source text
    ///
    /// # Arguments
    ///
    /// * `source` - Source text to parse
    /// * `start_position` - Starting byte position in source
    /// * `start_line` - Starting line number for error reporting
    #[must_use]
    #[allow(clippy::missing_const_for_fn)] // Can't be const due to Vec::new()
    pub fn new(source: &'a str, start_position: usize, start_line: usize) -> Self {
        Self {
            tracker: PositionTracker::new_at(
                source,
                start_position,
                u32::try_from(start_line).unwrap_or(u32::MAX),
                1,
            ),
            issues: Vec::new(),
            format: None,
        }
    }

    /// Create a new parser with a pre-known format for incremental parsing
    #[must_use]
    pub fn with_format(
        source: &'a str,
        format: &[&'a str],
        start_position: usize,
        start_line: u32,
    ) -> Self {
        Self {
            tracker: PositionTracker::new_at(source, start_position, start_line, 1),
            issues: Vec::new(),
            format: Some(format.to_vec()),
        }
    }

    /// Parse events section content
    ///
    /// Returns the parsed section and any issues encountered during parsing.
    /// Handles Format line parsing and event entry validation.
    ///
    /// # Returns
    ///
    /// Tuple of (`parsed_section`, `format_fields`, `parse_issues`, `final_position`, `final_line`)
    ///
    /// # Errors
    ///
    /// Returns an error if the events section contains malformed format lines or
    /// other unrecoverable syntax errors.
    pub fn parse(mut self) -> ParseResult<SectionParseResult<'a>> {
        let mut events = Vec::new();

        while !self.tracker.is_at_end() && !self.at_next_section() {
            self.skip_whitespace_and_comments();

            if self.tracker.is_at_end() || self.at_next_section() {
                break;
            }

            let line_start = self.tracker.checkpoint();
            let line = self.current_line().trim();

            if line.is_empty() {
                self.tracker.skip_line();
                continue;
            }

            if line.starts_with("Format:") {
                self.parse_format_line(line);
            } else if let Some(event) = self.parse_event_line_internal(line, &line_start) {
                events.push(event);
            }

            self.tracker.skip_line();
        }

        Ok((
            Section::Events(events),
            self.format,
            self.issues,
            self.tracker.offset(),
            self.tracker.line() as usize,
        ))
    }

    /// Parse format specification line
    fn parse_format_line(&mut self, line: &'a str) {
        if let Some(format_data) = line.strip_prefix("Format:") {
            let fields: Vec<&'a str> = format_data.split(',').map(str::trim).collect();
            self.format = Some(fields);
        }
    }

    /// Get current line from source
    pub(super) fn current_line(&self) -> &'a str {
        let remaining = self.tracker.remaining();
        let end = remaining.find('\n').unwrap_or(remaining.len());
        &remaining[..end]
    }

    /// Check if at start of next section
    #[must_use]
    fn at_next_section(&self) -> bool {
        self.tracker.remaining().trim_start().starts_with('[')
    }

    /// Skip whitespace and comment lines
    fn skip_whitespace_and_comments(&mut self) {
        loop {
            self.tracker.skip_whitespace();

            let remaining = self.tracker.remaining();
            if remaining.is_empty() {
                break;
            }

            if remaining.starts_with(';') || remaining.starts_with('#') {
                self.tracker.skip_line();
                continue;
            }

            // Check for newlines in whitespace
            if remaining.starts_with('\n') {
                self.tracker.advance(1);
                continue;
            }

            break;
        }
    }

    /// Get accumulated parse issues
    #[must_use]
    pub fn issues(self) -> Vec<ParseIssue> {
        self.issues
    }

    /// Get format specification
    #[must_use]
    pub const fn format(&self) -> Option<&Vec<&'a str>> {
        self.format.as_ref()
    }
}