use crate::parser::ast::{FileItem, Span};
use crate::parser::{parse_str, Rule};
#[derive(Debug, Clone)]
pub struct PartialParseResult {
pub items: Vec<FileItem>,
pub errors: Vec<PartialParseError>,
}
#[derive(Debug, Clone)]
pub struct PartialParseError {
pub line: usize,
pub message: String,
pub span: Option<Span>,
}
pub fn infer_rule_from_line(line: &str) -> Option<Rule> {
let trimmed = line.trim_start();
if trimmed.is_empty() {
return None;
}
let first_char = trimmed.chars().next()?;
match first_char {
'*' | '*' => Some(Rule::global_scene_scope),
'・' | '-' => Some(Rule::local_scene_line),
'&' | '&' => Some(Rule::file_attr_line),
'@' | '@' => Some(Rule::file_word_line),
'%' | '%' => Some(Rule::actor_scope),
'$' | '$' => Some(Rule::var_set_line),
'>' | '>' => Some(Rule::call_scene_line),
'!' | '!' => Some(Rule::cue_cmd_line),
'#' | '#' => Some(Rule::or_comment_eol),
'`' if trimmed.starts_with("```") => Some(Rule::code_scope),
_ => {
if trimmed.contains(':') || trimmed.contains(':') {
Some(Rule::action_line)
} else {
None
}
}
}
}
#[derive(Debug)]
struct SourceChunk {
text: String,
start_line: usize, }
fn split_by_scope_markers(source: &str) -> Vec<SourceChunk> {
let mut chunks = Vec::new();
let mut current_lines: Vec<&str> = Vec::new();
let mut current_start_line: usize = 1;
let lines: Vec<&str> = source.lines().collect();
for (i, line) in lines.iter().enumerate() {
let line_num = i + 1; let trimmed = line.trim_start();
let is_scope_boundary = if trimmed.is_empty() {
false
} else {
let first_char = trimmed.chars().next().unwrap_or(' ');
matches!(
first_char,
'*' | '*' | '%' | '%' | '&' | '&' | '@' | '@'
)
};
if is_scope_boundary && !current_lines.is_empty() {
let mut text = current_lines.join("\n");
text.push('\n');
chunks.push(SourceChunk {
text,
start_line: current_start_line,
});
current_lines.clear();
current_start_line = line_num;
}
if current_lines.is_empty() {
current_start_line = line_num;
}
current_lines.push(line);
}
if !current_lines.is_empty() {
let mut text = current_lines.join("\n");
text.push('\n');
chunks.push(SourceChunk {
text,
start_line: current_start_line,
});
}
chunks
}
pub fn parse_str_partial(source: &str) -> PartialParseResult {
if let Ok(pasta_file) = parse_str(source, "<partial>") {
return PartialParseResult {
items: pasta_file.items,
errors: vec![],
};
}
let mut partial_items = Vec::new();
let mut partial_errors = Vec::new();
let chunks = split_by_scope_markers(source);
for chunk in chunks {
let first_line = chunk.text.lines().next().unwrap_or("");
let rule = infer_rule_from_line(first_line);
if rule.is_some() {
let parse_result = try_parse_chunk(&chunk.text);
match parse_result {
Ok(items) => {
partial_items.extend(items);
continue;
}
Err(_) => {
for (line_idx, line) in chunk.text.lines().enumerate() {
let line_num = chunk.start_line + line_idx;
if line.trim().is_empty() {
continue;
}
if infer_rule_from_line(line).is_some() {
match try_parse_chunk(line) {
Ok(items) => partial_items.extend(items),
Err(e) => {
partial_errors.push(PartialParseError {
line: line_num,
message: e.to_string(),
span: None,
});
}
}
} else {
partial_errors.push(PartialParseError {
line: line_num,
message: "Unable to infer parse rule for line".to_string(),
span: None,
});
}
}
}
}
} else {
for (line_idx, line) in chunk.text.lines().enumerate() {
let line_num = chunk.start_line + line_idx;
if line.trim().is_empty() {
continue;
}
partial_errors.push(PartialParseError {
line: line_num,
message: "Unable to infer parse rule for line".to_string(),
span: None,
});
}
}
}
PartialParseResult {
items: partial_items,
errors: partial_errors,
}
}
fn try_parse_chunk(text: &str) -> Result<Vec<FileItem>, String> {
match parse_str(text, "<partial>") {
Ok(file) => Ok(file.items),
Err(e) => Err(format!("{}", e)),
}
}