use crate::{
parser::{
ast::{ScriptInfo, Section},
errors::{IssueCategory, IssueSeverity, ParseIssue},
position_tracker::PositionTracker,
sections::ScriptInfoParseResult,
ParseResult,
},
ScriptVersion,
};
use alloc::vec::Vec;
pub struct ScriptInfoParser<'a> {
tracker: PositionTracker<'a>,
issues: Vec<ParseIssue>,
}
impl<'a> ScriptInfoParser<'a> {
#[must_use]
#[allow(clippy::missing_const_for_fn)] 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(),
}
}
pub fn parse(mut self) -> ParseResult<ScriptInfoParseResult<'a>> {
let section_start = self.tracker.checkpoint();
let mut fields = Vec::new();
let mut detected_version = None;
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 let Some(colon_pos) = line.find(':') {
let key = line[..colon_pos].trim();
let value = line[colon_pos + 1..].trim();
if key == "ScriptType" {
if let Some(version) = ScriptVersion::from_header(value) {
detected_version = Some(version);
}
}
fields.push((key, value));
} else {
self.issues.push(ParseIssue::new(
IssueSeverity::Warning,
IssueCategory::Format,
"Invalid script info line format".into(),
line_start.line() as usize,
));
}
self.tracker.skip_line();
}
let span = self.tracker.span_from(§ion_start);
let section = Section::ScriptInfo(ScriptInfo { fields, span });
Ok((
section,
detected_version,
self.issues,
self.tracker.offset(),
self.tracker.line() as usize,
))
}
fn current_line(&self) -> &'a str {
let remaining = self.tracker.remaining();
let end = remaining.find('\n').unwrap_or(remaining.len());
&remaining[..end]
}
fn at_next_section(&self) -> bool {
self.tracker.remaining().trim_start().starts_with('[')
}
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;
}
if remaining.starts_with('\n') {
self.tracker.advance(1);
continue;
}
break;
}
}
#[must_use]
pub fn issues(self) -> Vec<ParseIssue> {
self.issues
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(feature = "std"))]
use alloc::format;
#[test]
fn parse_empty_section() {
let parser = ScriptInfoParser::new("", 0, 1);
let result = parser.parse();
assert!(result.is_ok());
let (section, version, ..) = result.unwrap();
if let Section::ScriptInfo(info) = section {
assert!(info.fields.is_empty());
assert_eq!(info.span.start, 0);
assert_eq!(info.span.end, 0);
} else {
panic!("Expected ScriptInfo section");
}
assert!(version.is_none());
}
#[test]
fn parse_basic_fields() {
let content = "Title: Test Script\nScriptType: v4.00+\n";
let parser = ScriptInfoParser::new(content, 0, 1);
let result = parser.parse();
assert!(result.is_ok());
let (section, version, ..) = result.unwrap();
if let Section::ScriptInfo(info) = section {
assert_eq!(info.fields.len(), 2);
assert_eq!(info.get_field("Title"), Some("Test Script"));
assert_eq!(info.get_field("ScriptType"), Some("v4.00+"));
assert_eq!(info.span.start, 0);
assert_eq!(info.span.end, content.len());
assert_eq!(info.span.line, 1);
assert_eq!(info.span.column, 1);
} else {
panic!("Expected ScriptInfo section");
}
assert!(version.is_some());
}
#[test]
fn skip_comments_and_whitespace() {
let content = "; Comment\n# Another comment\n\nTitle: Test\n";
let parser = ScriptInfoParser::new(content, 0, 1);
let result = parser.parse();
assert!(result.is_ok());
let (section, ..) = result.unwrap();
if let Section::ScriptInfo(info) = section {
assert_eq!(info.fields.len(), 1);
assert_eq!(info.get_field("Title"), Some("Test"));
} else {
panic!("Expected ScriptInfo section");
}
}
#[test]
fn handle_invalid_lines() {
let content = "Title: Test\nInvalidLine\nAuthor: Someone\n";
let parser = ScriptInfoParser::new(content, 0, 1);
let result = parser.parse();
assert!(result.is_ok());
let (section, _, issues, ..) = result.unwrap();
if let Section::ScriptInfo(info) = section {
assert_eq!(info.fields.len(), 2);
assert_eq!(info.get_field("Title"), Some("Test"));
assert_eq!(info.get_field("Author"), Some("Someone"));
} else {
panic!("Expected ScriptInfo section");
}
assert_eq!(issues.len(), 1);
assert_eq!(issues[0].severity, IssueSeverity::Warning);
}
#[test]
fn parse_with_position_tracking() {
let prefix = "Some prefix\n"; let section_content = "Title: Test\nAuthor: Someone\n";
let full_content = format!("{prefix}{section_content}");
let parser = ScriptInfoParser::new(&full_content, 12, 2);
let result = parser.parse();
assert!(result.is_ok());
let (section, _, _, final_pos, final_line) = result.unwrap();
if let Section::ScriptInfo(info) = section {
assert_eq!(info.fields.len(), 2);
assert_eq!(info.fields[0], ("Title", "Test"));
assert_eq!(info.fields[1], ("Author", "Someone"));
assert_eq!(info.span.start, 12);
assert_eq!(info.span.end, 12 + section_content.len());
assert_eq!(info.span.line, 2);
assert_eq!(info.span.column, 1);
} else {
panic!("Expected ScriptInfo section");
}
assert_eq!(final_pos, 12 + section_content.len());
assert_eq!(final_line, 4); }
}