// src/parser.rs use regex::Regex; use std::collections::HashSet; pub struct ScriptParser { math_operators: Vec<(Regex, &'static str)>, number_re: Regex, variable_re: Regex, } impl ScriptParser { pub fn new() -> Self { let math_operators = vec![ // Basic arithmetic (Regex::new(r"\(\s*\+\s+").unwrap(), "(decimal-add "), (Regex::new(r"\(\s*-\s+").unwrap(), "(decimal-sub "), (Regex::new(r"\(\s*\*\s+").unwrap(), "(decimal-mul "), (Regex::new(r"\(\s*/\s+").unwrap(), "(decimal-div "), // Power and advanced operations (Regex::new(r"\(\s*\^\s+").unwrap(), "(decimal-pow "), (Regex::new(r"\(\s*\*\*\s+").unwrap(), "(decimal-pow "), (Regex::new(r"\(\s*pow\s+").unwrap(), "(decimal-pow "), (Regex::new(r"\(\s*sqrt\s+").unwrap(), "(decimal-sqrt "), // Logarithmic functions (Regex::new(r"\(\s*ln\s+").unwrap(), "(decimal-ln "), (Regex::new(r"\(\s*log\s+").unwrap(), "(decimal-ln "), (Regex::new(r"\(\s*log10\s+").unwrap(), "(decimal-log10 "), (Regex::new(r"\(\s*exp\s+").unwrap(), "(decimal-exp "), // Trigonometric functions (Regex::new(r"\(\s*sin\s+").unwrap(), "(decimal-sin "), (Regex::new(r"\(\s*cos\s+").unwrap(), "(decimal-cos "), (Regex::new(r"\(\s*tan\s+").unwrap(), "(decimal-tan "), // Comparison operators (Regex::new(r"\(\s*>\s+").unwrap(), "(decimal-gt "), (Regex::new(r"\(\s*<\s+").unwrap(), "(decimal-lt "), (Regex::new(r"\(\s*=\s+").unwrap(), "(decimal-eq "), (Regex::new(r"\(\s*>=\s+").unwrap(), "(decimal-gte "), (Regex::new(r"\(\s*<=\s+").unwrap(), "(decimal-lte "), // Utility functions (Regex::new(r"\(\s*abs\s+").unwrap(), "(decimal-abs "), (Regex::new(r"\(\s*min\s+").unwrap(), "(decimal-min "), (Regex::new(r"\(\s*max\s+").unwrap(), "(decimal-max "), (Regex::new(r"\(\s*round\s+").unwrap(), "(decimal-round "), ]; ScriptParser { math_operators, // VALID REGEX: // This captures the preceding delimiter (group 1) and the number (group 2) separately. // This avoids lookarounds and allows us to reconstruct the string correctly. number_re: Regex::new(r"(^|[\s\(])(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)").unwrap(), variable_re: Regex::new(r"\$([^\s)]+)").unwrap(), } } /// Transform a script by converting math operations and numbers to decimal functions pub fn transform(&self, script: &str) -> String { let mut transformed = script.to_string(); // CORRECT ORDER: Functions first, then variables, then numbers. transformed = self.replace_math_functions(&transformed); transformed = self.replace_variable_references(&transformed); transformed = self.convert_numbers_to_strings(&transformed); transformed } /// Convert all unquoted numeric literals to quoted strings fn convert_numbers_to_strings(&self, script: &str) -> String { let parts: Vec<&str> = script.split('"').collect(); let mut result = String::new(); for (i, part) in parts.iter().enumerate() { if i % 2 == 0 { // Even indices are outside quotes - process them let processed = self.number_re.replace_all(part, |caps: ®ex::Captures| { // Reconstruct the string: // caps[1] is the delimiter (space, '(', or start of string) // caps[2] is the number itself // We put the delimiter back and wrap the number in quotes. format!("{}\"{}\"", &caps[1], &caps[2]) }); result.push_str(&processed); } else { // Odd indices are inside quotes - keep as is result.push_str(part); } if i < parts.len() - 1 { result.push('"'); } } result } /// Replace math function calls with decimal equivalents fn replace_math_functions(&self, script: &str) -> String { let mut result = script.to_string(); for (pattern, replacement) in &self.math_operators { result = pattern.replace_all(&result, *replacement).to_string(); } result } /// Replace variable references ($var) with function calls fn replace_variable_references(&self, script: &str) -> String { self.variable_re.replace_all(script, |caps: ®ex::Captures| { format!("(get-var \"{}\")", &caps[1]) }).to_string() } /// Extract dependencies from script (useful for analysis) pub fn extract_dependencies(&self, script: &str) -> HashSet { let mut dependencies = HashSet::new(); for cap in self.variable_re.captures_iter(script) { dependencies.insert(cap[1].to_string()); } dependencies } } impl Default for ScriptParser { fn default() -> Self { Self::new() } }