ass_core/parser/sections/
script_info.rs1use crate::{
7 parser::{
8 ast::{ScriptInfo, Section},
9 errors::{IssueCategory, IssueSeverity, ParseIssue},
10 position_tracker::PositionTracker,
11 sections::ScriptInfoParseResult,
12 ParseResult,
13 },
14 ScriptVersion,
15};
16use alloc::vec::Vec;
17
18pub struct ScriptInfoParser<'a> {
29 tracker: PositionTracker<'a>,
31 issues: Vec<ParseIssue>,
33}
34
35impl<'a> ScriptInfoParser<'a> {
36 #[must_use]
44 #[allow(clippy::missing_const_for_fn)] pub fn new(source: &'a str, start_position: usize, start_line: usize) -> Self {
46 Self {
47 tracker: PositionTracker::new_at(
48 source,
49 start_position,
50 u32::try_from(start_line).unwrap_or(u32::MAX),
51 1,
52 ),
53 issues: Vec::new(),
54 }
55 }
56
57 pub fn parse(mut self) -> ParseResult<ScriptInfoParseResult<'a>> {
71 let section_start = self.tracker.checkpoint();
72 let mut fields = Vec::new();
73 let mut detected_version = None;
74
75 while !self.tracker.is_at_end() && !self.at_next_section() {
76 self.skip_whitespace_and_comments();
77
78 if self.tracker.is_at_end() || self.at_next_section() {
79 break;
80 }
81
82 let line_start = self.tracker.checkpoint();
83 let line = self.current_line().trim();
84
85 if line.is_empty() {
86 self.tracker.skip_line();
87 continue;
88 }
89
90 if let Some(colon_pos) = line.find(':') {
91 let key = line[..colon_pos].trim();
92 let value = line[colon_pos + 1..].trim();
93
94 if key == "ScriptType" {
95 if let Some(version) = ScriptVersion::from_header(value) {
96 detected_version = Some(version);
97 }
98 }
99
100 fields.push((key, value));
101 } else {
102 self.issues.push(ParseIssue::new(
103 IssueSeverity::Warning,
104 IssueCategory::Format,
105 "Invalid script info line format".into(),
106 line_start.line() as usize,
107 ));
108 }
109
110 self.tracker.skip_line();
111 }
112
113 let span = self.tracker.span_from(§ion_start);
114 let section = Section::ScriptInfo(ScriptInfo { fields, span });
115
116 Ok((
117 section,
118 detected_version,
119 self.issues,
120 self.tracker.offset(),
121 self.tracker.line() as usize,
122 ))
123 }
124
125 fn current_line(&self) -> &'a str {
127 let remaining = self.tracker.remaining();
128 let end = remaining.find('\n').unwrap_or(remaining.len());
129 &remaining[..end]
130 }
131
132 fn at_next_section(&self) -> bool {
134 self.tracker.remaining().trim_start().starts_with('[')
135 }
136
137 fn skip_whitespace_and_comments(&mut self) {
139 loop {
140 self.tracker.skip_whitespace();
141
142 let remaining = self.tracker.remaining();
143 if remaining.is_empty() {
144 break;
145 }
146
147 if remaining.starts_with(';') || remaining.starts_with('#') {
148 self.tracker.skip_line();
149 continue;
150 }
151
152 if remaining.starts_with('\n') {
154 self.tracker.advance(1);
155 continue;
156 }
157
158 break;
159 }
160 }
161
162 #[must_use]
164 pub fn issues(self) -> Vec<ParseIssue> {
165 self.issues
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 #[cfg(not(feature = "std"))]
173 use alloc::format;
174
175 #[test]
176 fn parse_empty_section() {
177 let parser = ScriptInfoParser::new("", 0, 1);
178 let result = parser.parse();
179 assert!(result.is_ok());
180
181 let (section, version, ..) = result.unwrap();
182 if let Section::ScriptInfo(info) = section {
183 assert!(info.fields.is_empty());
184 assert_eq!(info.span.start, 0);
185 assert_eq!(info.span.end, 0);
186 } else {
187 panic!("Expected ScriptInfo section");
188 }
189 assert!(version.is_none());
190 }
191
192 #[test]
193 fn parse_basic_fields() {
194 let content = "Title: Test Script\nScriptType: v4.00+\n";
195 let parser = ScriptInfoParser::new(content, 0, 1);
196 let result = parser.parse();
197 assert!(result.is_ok());
198
199 let (section, version, ..) = result.unwrap();
200 if let Section::ScriptInfo(info) = section {
201 assert_eq!(info.fields.len(), 2);
202 assert_eq!(info.get_field("Title"), Some("Test Script"));
203 assert_eq!(info.get_field("ScriptType"), Some("v4.00+"));
204 assert_eq!(info.span.start, 0);
205 assert_eq!(info.span.end, content.len());
206 assert_eq!(info.span.line, 1);
207 assert_eq!(info.span.column, 1);
208 } else {
209 panic!("Expected ScriptInfo section");
210 }
211 assert!(version.is_some());
212 }
213
214 #[test]
215 fn skip_comments_and_whitespace() {
216 let content = "; Comment\n# Another comment\n\nTitle: Test\n";
217 let parser = ScriptInfoParser::new(content, 0, 1);
218 let result = parser.parse();
219 assert!(result.is_ok());
220
221 let (section, ..) = result.unwrap();
222 if let Section::ScriptInfo(info) = section {
223 assert_eq!(info.fields.len(), 1);
224 assert_eq!(info.get_field("Title"), Some("Test"));
225 } else {
226 panic!("Expected ScriptInfo section");
227 }
228 }
229
230 #[test]
231 fn handle_invalid_lines() {
232 let content = "Title: Test\nInvalidLine\nAuthor: Someone\n";
233 let parser = ScriptInfoParser::new(content, 0, 1);
234 let result = parser.parse();
235 assert!(result.is_ok());
236
237 let (section, _, issues, ..) = result.unwrap();
238 if let Section::ScriptInfo(info) = section {
239 assert_eq!(info.fields.len(), 2);
240 assert_eq!(info.get_field("Title"), Some("Test"));
241 assert_eq!(info.get_field("Author"), Some("Someone"));
242 } else {
243 panic!("Expected ScriptInfo section");
244 }
245
246 assert_eq!(issues.len(), 1);
248 assert_eq!(issues[0].severity, IssueSeverity::Warning);
249 }
250
251 #[test]
252 fn parse_with_position_tracking() {
253 let prefix = "Some prefix\n"; let section_content = "Title: Test\nAuthor: Someone\n";
256 let full_content = format!("{prefix}{section_content}");
257
258 let parser = ScriptInfoParser::new(&full_content, 12, 2);
260 let result = parser.parse();
261 assert!(result.is_ok());
262
263 let (section, _, _, final_pos, final_line) = result.unwrap();
264 if let Section::ScriptInfo(info) = section {
265 assert_eq!(info.fields.len(), 2);
266 assert_eq!(info.fields[0], ("Title", "Test"));
267 assert_eq!(info.fields[1], ("Author", "Someone"));
268 assert_eq!(info.span.start, 12);
269 assert_eq!(info.span.end, 12 + section_content.len());
270 assert_eq!(info.span.line, 2);
271 assert_eq!(info.span.column, 1);
272 } else {
273 panic!("Expected ScriptInfo section");
274 }
275
276 assert_eq!(final_pos, 12 + section_content.len());
277 assert_eq!(final_line, 4); }
279}