Skip to main content

ass_core/parser/sections/styles/
parser.rs

1//! Construction, driving, and scanning helpers for [`StylesParser`].
2//!
3//! Hosts the parser constructors, the top-level [`StylesParser::parse`] driver,
4//! and the whitespace/section scanning helpers used while iterating lines.
5
6use super::StylesParser;
7use crate::parser::{
8    ast::Section, position_tracker::PositionTracker, sections::SectionParseResult, ParseResult,
9};
10use alloc::{vec, vec::Vec};
11
12impl<'a> StylesParser<'a> {
13    /// Create new styles parser for source text
14    ///
15    /// # Arguments
16    ///
17    /// * `source` - Source text to parse
18    /// * `start_position` - Starting byte position in source
19    /// * `start_line` - Starting line number for error reporting
20    #[must_use]
21    #[allow(clippy::missing_const_for_fn)] // Can't be const due to Vec::new()
22    pub fn new(source: &'a str, start_position: usize, start_line: usize) -> Self {
23        Self {
24            tracker: PositionTracker::new_at(
25                source,
26                start_position,
27                u32::try_from(start_line).unwrap_or(u32::MAX),
28                1,
29            ),
30            issues: Vec::new(),
31            format: None,
32        }
33    }
34
35    /// Create a new parser with a pre-known format for incremental parsing
36    #[must_use]
37    pub fn with_format(
38        source: &'a str,
39        format: &[&'a str],
40        start_position: usize,
41        start_line: u32,
42    ) -> Self {
43        Self {
44            tracker: PositionTracker::new_at(source, start_position, start_line, 1),
45            issues: Vec::new(),
46            format: Some(format.to_vec()),
47        }
48    }
49
50    /// Parse styles section content
51    ///
52    /// Returns the parsed section and any issues encountered during parsing.
53    /// Handles Format line parsing and style entry validation.
54    ///
55    /// # Returns
56    ///
57    /// Tuple of (`parsed_section`, `format_fields`, `parse_issues`, `final_position`, `final_line`)
58    ///
59    /// # Errors
60    ///
61    /// Returns an error if the styles section contains malformed format lines or
62    /// other unrecoverable syntax errors.
63    pub fn parse(mut self) -> ParseResult<SectionParseResult<'a>> {
64        let mut styles = Vec::new();
65
66        while !self.tracker.is_at_end() && !self.at_next_section() {
67            self.skip_whitespace_and_comments();
68
69            if self.tracker.is_at_end() || self.at_next_section() {
70                break;
71            }
72
73            let line_start = self.tracker.checkpoint();
74            let line = self.current_line().trim();
75
76            if line.is_empty() {
77                self.tracker.skip_line();
78                continue;
79            }
80
81            if line.starts_with("Format:") {
82                self.parse_format_line(line);
83            } else if line.starts_with("Style:") {
84                if let Some(style_data) = line.strip_prefix("Style:") {
85                    if let Some(style) =
86                        self.parse_style_line_internal(style_data.trim(), &line_start)
87                    {
88                        styles.push(style);
89                    }
90                }
91            }
92
93            self.tracker.skip_line();
94        }
95
96        // If no explicit format was provided but styles were parsed, use default format
97        let format_to_return = if self.format.is_none() && !styles.is_empty() {
98            Some(vec![
99                "Name",
100                "Fontname",
101                "Fontsize",
102                "PrimaryColour",
103                "SecondaryColour",
104                "OutlineColour",
105                "BackColour",
106                "Bold",
107                "Italic",
108                "Underline",
109                "StrikeOut",
110                "ScaleX",
111                "ScaleY",
112                "Spacing",
113                "Angle",
114                "BorderStyle",
115                "Outline",
116                "Shadow",
117                "Alignment",
118                "MarginL",
119                "MarginR",
120                "MarginV",
121                "Encoding",
122            ])
123        } else {
124            self.format
125        };
126
127        Ok((
128            Section::Styles(styles),
129            format_to_return,
130            self.issues,
131            self.tracker.offset(),
132            self.tracker.line() as usize,
133        ))
134    }
135
136    /// Parse format specification line
137    fn parse_format_line(&mut self, line: &'a str) {
138        if let Some(format_data) = line.strip_prefix("Format:") {
139            let fields: Vec<&'a str> = format_data.split(',').map(str::trim).collect();
140            self.format = Some(fields);
141        }
142    }
143
144    /// Get current line from source
145    pub(super) fn current_line(&self) -> &'a str {
146        let remaining = self.tracker.remaining();
147        let end = remaining.find('\n').unwrap_or(remaining.len());
148        &remaining[..end]
149    }
150
151    /// Check if at start of next section
152    fn at_next_section(&self) -> bool {
153        self.tracker.remaining().trim_start().starts_with('[')
154    }
155
156    /// Skip whitespace and comment lines
157    fn skip_whitespace_and_comments(&mut self) {
158        loop {
159            self.tracker.skip_whitespace();
160
161            let remaining = self.tracker.remaining();
162            if remaining.is_empty() {
163                break;
164            }
165
166            if remaining.starts_with(';') || remaining.starts_with('#') {
167                self.tracker.skip_line();
168                continue;
169            }
170
171            // Check for newlines in whitespace
172            if remaining.starts_with('\n') {
173                self.tracker.advance(1);
174                continue;
175            }
176
177            break;
178        }
179    }
180}