Skip to main content

ass_core/parser/sections/script_info/
parser.rs

1//! Parser type and implementation for the Script Info section.
2//!
3//! Defines [`ScriptInfoParser`], which parses key-value pairs from the
4//! script info section and handles special fields like `ScriptType` that
5//! affect parsing behavior.
6
7use crate::{
8    parser::{
9        ast::{ScriptInfo, Section},
10        errors::{IssueCategory, IssueSeverity, ParseIssue},
11        position_tracker::PositionTracker,
12        sections::ScriptInfoParseResult,
13        ParseResult,
14    },
15    ScriptVersion,
16};
17use alloc::vec::Vec;
18
19/// Parser for [Script Info] section content
20///
21/// Parses key-value pairs from the script info section and handles
22/// special fields like `ScriptType` that affect parsing behavior.
23///
24/// # Performance
25///
26/// - Time complexity: O(n) for n lines in section
27/// - Memory: Zero allocations via lifetime-generic spans
28/// - Target: <0.5ms for typical script info sections
29pub struct ScriptInfoParser<'a> {
30    /// Position tracker for accurate span generation
31    tracker: PositionTracker<'a>,
32    /// Parse issues and warnings collected during parsing
33    issues: Vec<ParseIssue>,
34}
35
36impl<'a> ScriptInfoParser<'a> {
37    /// Create new script info parser for source text
38    ///
39    /// # Arguments
40    ///
41    /// * `source` - Source text to parse
42    /// * `start_position` - Starting byte position in source
43    /// * `start_line` - Starting line number for error reporting
44    #[must_use]
45    #[allow(clippy::missing_const_for_fn)] // Can't be const due to Vec::new()
46    pub fn new(source: &'a str, start_position: usize, start_line: usize) -> Self {
47        Self {
48            tracker: PositionTracker::new_at(
49                source,
50                start_position,
51                u32::try_from(start_line).unwrap_or(u32::MAX),
52                1,
53            ),
54            issues: Vec::new(),
55        }
56    }
57
58    /// Parse script info section content
59    ///
60    /// Returns the parsed section and any issues encountered during parsing.
61    /// Handles `ScriptType` field detection and version updating.
62    ///
63    /// # Returns
64    ///
65    /// Tuple of (`parsed_section`, `detected_version`, `parse_issues`, `final_position`, `final_line`)
66    ///
67    /// # Errors
68    ///
69    /// Returns an error if the script info section contains malformed key-value pairs or
70    /// other unrecoverable syntax errors.
71    pub fn parse(mut self) -> ParseResult<ScriptInfoParseResult<'a>> {
72        let section_start = self.tracker.checkpoint();
73        let mut fields = Vec::new();
74        let mut detected_version = None;
75
76        while !self.tracker.is_at_end() && !self.at_next_section() {
77            self.skip_whitespace_and_comments();
78
79            if self.tracker.is_at_end() || self.at_next_section() {
80                break;
81            }
82
83            let line_start = self.tracker.checkpoint();
84            let line = self.current_line().trim();
85
86            if line.is_empty() {
87                self.tracker.skip_line();
88                continue;
89            }
90
91            if let Some(colon_pos) = line.find(':') {
92                let key = line[..colon_pos].trim();
93                let value = line[colon_pos + 1..].trim();
94
95                if key == "ScriptType" {
96                    if let Some(version) = ScriptVersion::from_header(value) {
97                        detected_version = Some(version);
98                    }
99                }
100
101                fields.push((key, value));
102            } else {
103                self.issues.push(ParseIssue::new(
104                    IssueSeverity::Warning,
105                    IssueCategory::Format,
106                    "Invalid script info line format".into(),
107                    line_start.line() as usize,
108                ));
109            }
110
111            self.tracker.skip_line();
112        }
113
114        let span = self.tracker.span_from(&section_start);
115        let section = Section::ScriptInfo(ScriptInfo { fields, span });
116
117        Ok((
118            section,
119            detected_version,
120            self.issues,
121            self.tracker.offset(),
122            self.tracker.line() as usize,
123        ))
124    }
125
126    /// Get current line from source
127    fn current_line(&self) -> &'a str {
128        let remaining = self.tracker.remaining();
129        let end = remaining.find('\n').unwrap_or(remaining.len());
130        &remaining[..end]
131    }
132
133    /// Check if at start of next section
134    fn at_next_section(&self) -> bool {
135        self.tracker.remaining().trim_start().starts_with('[')
136    }
137
138    /// Skip whitespace and comment lines
139    fn skip_whitespace_and_comments(&mut self) {
140        loop {
141            self.tracker.skip_whitespace();
142
143            let remaining = self.tracker.remaining();
144            if remaining.is_empty() {
145                break;
146            }
147
148            if remaining.starts_with(';') || remaining.starts_with('#') {
149                self.tracker.skip_line();
150                continue;
151            }
152
153            // Check for newlines in whitespace
154            if remaining.starts_with('\n') {
155                self.tracker.advance(1);
156                continue;
157            }
158
159            break;
160        }
161    }
162
163    /// Get accumulated parse issues
164    #[must_use]
165    pub fn issues(self) -> Vec<ParseIssue> {
166        self.issues
167    }
168}