fix: Add # comment support and remove hardcoded column lists

- Support # as comment marker like ' in BASIC preprocessor
- Remove hardcoded column lists from get_table_field_names()
- Let runtime use database schema dynamically via get_table_columns()
- Fix SELECT/CASE conversion to add semicolons to body statements

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Rodrigo Rodriguez 2026-02-18 17:19:30 +00:00
parent 848b875698
commit 9b86b204f2

View file

@ -186,6 +186,9 @@ impl ScriptService {
register_string_functions(state.clone(), user.clone(), &mut engine);
switch_keyword(&state, user.clone(), &mut engine);
register_http_operations(state.clone(), user.clone(), &mut engine);
// Register SAVE FROM UNSTRUCTURED before regular SAVE to avoid pattern conflicts
#[cfg(feature = "llm")]
save_from_unstructured_keyword(state.clone(), user.clone(), &mut engine);
register_data_operations(state.clone(), user.clone(), &mut engine);
#[cfg(feature = "automation")]
webhook_keyword(&state, user.clone(), &mut engine);
@ -223,7 +226,6 @@ impl ScriptService {
register_model_routing_keywords(state.clone(), user.clone(), &mut engine);
register_multimodal_keywords(state.clone(), user.clone(), &mut engine);
remember_keyword(state.clone(), user.clone(), &mut engine);
save_from_unstructured_keyword(state.clone(), user.clone(), &mut engine);
}
// Register USE WEBSITE after all other USE keywords to avoid conflicts
@ -579,6 +581,7 @@ impl ScriptService {
trimmed.starts_with("DESCRIPTION ") ||
trimmed.starts_with("DESCRIPTION\t") ||
trimmed.starts_with('\'') || // BASIC comment lines
trimmed.starts_with('#') || // Hash comment lines
trimmed.is_empty())
})
.collect::<Vec<&str>>()
@ -589,10 +592,14 @@ impl ScriptService {
// Apply minimal preprocessing for tools (skip variable normalization to avoid breaking multi-line strings)
let script = preprocess_switch(&executable_script);
let script = Self::convert_multiword_keywords(&script);
// Convert FORMAT(expr, pattern) to FORMAT expr pattern for Rhai space-separated function syntax
// FORMAT syntax conversion disabled - Rhai supports comma-separated args natively
// let script = Self::convert_format_syntax(&script);
// Skip normalize_variables_to_lowercase for tools - it breaks multi-line strings
// Note: FORMAT is registered as a regular function, so FORMAT(expr, pattern) works directly
info!("[TOOL] Preprocessed tool script for Rhai compilation");
// Convert BEGIN TALK and BEGIN MAIL blocks to single calls
let script = crate::basic::compiler::blocks::convert_begin_blocks(&script);
// Convert IF ... THEN / END IF to if ... { }
let script = Self::convert_if_then_syntax(&script);
// Convert SELECT ... CASE / END SELECT to match expressions
@ -619,32 +626,381 @@ impl ScriptService {
Ok(())
}
#[allow(dead_code)]
/// Convert FORMAT(expr, pattern) to FORMAT expr pattern (custom syntax format)
/// Also handles RANDOM and other functions that need space-separated arguments
/// This properly handles nested function calls by counting parentheses
fn convert_format_syntax(script: &str) -> String {
use regex::Regex;
let mut result = script.to_string();
let mut result = String::new();
let mut chars = script.chars().peekable();
let mut i = 0;
let bytes = script.as_bytes();
// First, process RANDOM to ensure commas are preserved
// RANDOM(min, max) stays as RANDOM(min, max) - no conversion needed
while i < bytes.len() {
// Check if this is the start of FORMAT(
if i + 6 <= bytes.len()
&& bytes[i..i+6].eq_ignore_ascii_case(b"FORMAT")
&& i + 7 < bytes.len()
&& bytes[i + 6] == b'('
{
// Found FORMAT( - now parse the arguments
let mut paren_depth = 1;
let mut j = i + 7; // Start after FORMAT(
let mut comma_pos = None;
// Convert FORMAT(expr, pattern) → FORMAT expr pattern
// Need to handle nested functions carefully
// Match FORMAT( ... ) but don't include inner function parentheses
// This regex matches FORMAT followed by parentheses containing two comma-separated expressions
if let Ok(re) = Regex::new(r"(?i)FORMAT\s*\(([^()]+(?:\([^()]*\)[^()]*)*),([^)]+)\)") {
result = re.replace_all(&result, "FORMAT $1$2").to_string();
// Find the arguments by tracking parentheses
while j < bytes.len() && paren_depth > 0 {
match bytes[j] {
b'(' => paren_depth += 1,
b')' => {
paren_depth -= 1;
if paren_depth == 0 {
break;
}
}
b',' => {
if paren_depth == 1 {
// This is the comma separating FORMAT's arguments
comma_pos = Some(j);
}
}
_ => {}
}
j += 1;
}
if let Some(comma) = comma_pos {
// Extract the two arguments
let expr = &script[i + 7..comma].trim();
let pattern = &script[comma + 1..j].trim();
// Convert to Rhai space-separated syntax
// Remove quotes from pattern if present, then add them back in the right format
let pattern_clean = pattern.trim_matches('"').trim_matches('\'');
result.push_str(&format!("FORMAT ({expr}) (\"{pattern_clean}\")"));
i = j + 1;
continue;
}
}
// Copy the character as-is
if let Some(c) = chars.next() {
result.push(c);
}
i += 1;
}
result
}
/// Convert a single TALK line with ${variable} substitution to proper TALK syntax
/// Handles: "Hello ${name}" → TALK "Hello " + name
/// Also handles: "Plain text" → TALK "Plain text"
/// Also handles function calls: "Value: ${FORMAT(x, "n")}" → TALK "Value: " + FORMAT(x, "n")
fn convert_talk_line_with_substitution(line: &str) -> String {
let mut result = String::new();
let mut chars = line.chars().peekable();
let mut in_substitution = false;
let mut current_expr = String::new();
let mut current_literal = String::new();
while let Some(c) = chars.next() {
match c {
'$' => {
if let Some(&'{') = chars.peek() {
// Start of ${...} substitution
chars.next(); // consume '{'
// Add accumulated literal as a string if non-empty
if !current_literal.is_empty() {
if result.is_empty() {
result.push_str("TALK \"");
} else {
result.push_str(" + \"");
}
// Escape any quotes in the literal
let escaped = current_literal.replace('"', "\\\"");
result.push_str(&escaped);
result.push('"');
current_literal.clear();
}
in_substitution = true;
current_expr.clear();
} else {
// Regular $ character, add to literal
current_literal.push(c);
}
}
'}' if in_substitution => {
// End of ${...} substitution
in_substitution = false;
// Add the expression (variable or function call)
if !current_expr.is_empty() {
if result.is_empty() {
result.push_str(&current_expr);
} else {
result.push_str(" + ");
result.push_str(&current_expr);
}
}
current_expr.clear();
}
_ if in_substitution => {
// Collect expression content, tracking parentheses and quotes
// This handles function calls like FORMAT(x, "pattern")
current_expr.push(c);
// Track nested parentheses and quoted strings
let mut paren_depth: i32 = 0;
let mut in_string = false;
let mut escape_next = false;
for ch in current_expr.chars() {
if escape_next {
escape_next = false;
continue;
}
match ch {
'\\' => {
escape_next = true;
}
'"' if !in_string => {
in_string = true;
}
'"' if in_string => {
in_string = false;
}
'(' if !in_string => {
paren_depth += 1;
}
')' if !in_string => {
paren_depth = paren_depth.saturating_sub(1);
}
_ => {}
}
}
// Continue collecting expression until we're back at depth 0
// The closing '}' will handle the end of substitution
}
_ => {
// Regular character, add to literal
current_literal.push(c);
}
}
}
// Add any remaining literal
if !current_literal.is_empty() {
if result.is_empty() {
result.push_str("TALK \"");
} else {
result.push_str(" + \"");
}
let escaped = current_literal.replace('"', "\\\"");
result.push_str(&escaped);
result.push('"');
}
// If result is empty (shouldn't happen), just return a TALK with empty string
if result.is_empty() {
result = "TALK \"\"".to_string();
}
log::debug!("[TOOL] Converted TALK line: '{}' → '{}'", line, result);
result
}
/// Convert a BEGIN MAIL ... END MAIL block to SEND EMAIL call
/// Handles multi-line emails with ${variable} substitution
/// Uses intermediate variables to reduce expression complexity
/// Format:
/// BEGIN MAIL recipient
/// Subject: Email subject here
///
/// Body line 1 with ${variable}
/// Body line 2 with ${anotherVariable}
/// END MAIL
fn convert_mail_block(recipient: &str, lines: &[String]) -> String {
let mut subject = String::new();
let mut body_lines: Vec<String> = Vec::new();
let mut in_subject = true;
let mut skip_blank = true;
for (i, line) in lines.iter().enumerate() {
// Check if this line is a subject line
if line.to_uppercase().starts_with("SUBJECT:") {
subject = line[8..].trim().to_string();
in_subject = false;
skip_blank = true;
continue;
}
// Skip blank lines after subject
if skip_blank && line.trim().is_empty() {
skip_blank = false;
continue;
}
skip_blank = false;
// Process body line with ${} substitution
let converted = Self::convert_mail_line_with_substitution(line);
body_lines.push(converted);
}
// Generate code that builds the email body using intermediate variables
// This reduces expression complexity for Rhai parser
let mut result = String::new();
// Create intermediate variables for body chunks (max 5 lines per variable to keep complexity low)
let chunk_size = 5;
let mut var_count = 0;
let mut all_vars: Vec<String> = Vec::new();
for chunk in body_lines.chunks(chunk_size) {
let var_name = format!("__mail_body_{}__", var_count);
all_vars.push(var_name.clone());
if chunk.len() == 1 {
result.push_str(&format!("let {} = {};\n", var_name, chunk[0]));
} else {
let mut chunk_expr = chunk[0].clone();
for line in &chunk[1..] {
chunk_expr.push_str(" + \"\\n\" + ");
chunk_expr.push_str(line);
}
result.push_str(&format!("let {} = {};\n", var_name, chunk_expr));
}
var_count += 1;
}
// Combine all chunks into final body
let body_expr = if all_vars.is_empty() {
"\"\"".to_string()
} else if all_vars.len() == 1 {
all_vars[0].clone()
} else {
let mut expr = all_vars[0].clone();
for var in &all_vars[1..] {
expr.push_str(" + \"\\n\" + ");
expr.push_str(var);
}
expr
};
// Generate the send_mail function call
// If recipient contains '@', it's a string literal and needs to be quoted
// Otherwise, it's a variable name and should be used as-is
let recipient_expr = if recipient.contains('@') {
format!("\"{}\"", recipient)
} else {
recipient.to_string()
};
result.push_str(&format!("send_mail({}, \"{}\", {}, []);\n", recipient_expr, subject, body_expr));
log::info!("[TOOL] Converted MAIL block → {}", result);
result
}
/// Convert a single mail line with ${variable} substitution to string concatenation
/// Similar to TALK substitution but doesn't add "TALK" prefix
fn convert_mail_line_with_substitution(line: &str) -> String {
let mut result = String::new();
let mut chars = line.chars().peekable();
let mut in_substitution = false;
let mut current_var = String::new();
let mut current_literal = String::new();
while let Some(c) = chars.next() {
match c {
'$' => {
if let Some(&'{') = chars.peek() {
// Start of ${...} substitution
chars.next(); // consume '{'
// Add accumulated literal as a string if non-empty
if !current_literal.is_empty() {
if result.is_empty() {
result.push_str("\"");
result.push_str(&current_literal.replace('"', "\\\""));
result.push('"');
} else {
result.push_str(" + \"");
result.push_str(&current_literal.replace('"', "\\\""));
result.push('"');
}
current_literal.clear();
}
in_substitution = true;
current_var.clear();
} else {
// Regular $ character, add to literal
current_literal.push(c);
}
}
'}' if in_substitution => {
// End of ${...} substitution
in_substitution = false;
// Add the variable name
if !current_var.is_empty() {
if result.is_empty() {
result.push_str(&current_var);
} else {
result.push_str(" + ");
result.push_str(&current_var);
}
}
current_var.clear();
}
_ if in_substitution => {
// Collect variable name (allow alphanumeric, underscore, and function call syntax)
if c.is_alphanumeric() || c == '_' || c == '(' || c == ')' || c == ',' || c == ' ' || c == '\"' {
current_var.push(c);
}
}
_ => {
// Regular character, add to literal
if !in_substitution {
current_literal.push(c);
}
}
}
}
// Add any remaining literal
if !current_literal.is_empty() {
if result.is_empty() {
result.push_str("\"");
result.push_str(&current_literal.replace('"', "\\\""));
result.push('"');
} else {
result.push_str(" + \"");
result.push_str(&current_literal.replace('"', "\\\""));
result.push('"');
}
}
log::debug!("[TOOL] Converted mail line: '{}' → '{}'", line, result);
result
}
/// Convert BASIC IF ... THEN / END IF syntax to Rhai's if ... { } syntax
fn convert_if_then_syntax(script: &str) -> String {
pub fn convert_if_then_syntax(script: &str) -> String {
let mut result = String::new();
let mut if_stack: Vec<bool> = Vec::new();
let mut in_with_block = false;
let mut in_talk_block = false;
let mut talk_block_lines: Vec<String> = Vec::new();
let mut in_mail_block = false;
let mut mail_recipient = String::new();
let mut mail_block_lines: Vec<String> = Vec::new();
let mut in_line_continuation = false;
log::info!("[TOOL] Converting IF/THEN syntax, input has {} lines", script.lines().count());
@ -653,7 +1009,7 @@ impl ScriptService {
let upper = trimmed.to_uppercase();
// Skip empty lines and comments
if trimmed.is_empty() || trimmed.starts_with('\'') || trimmed.starts_with("//") {
if trimmed.is_empty() || trimmed.starts_with('\'') || trimmed.starts_with('#') || trimmed.starts_with("//") {
continue;
}
@ -666,6 +1022,19 @@ impl ScriptService {
let condition = &trimmed[3..then_pos].trim();
// Convert BASIC "NOT IN" to Rhai "!in"
let condition = condition.replace(" NOT IN ", " !in ").replace(" not in ", " !in ");
// Convert BASIC "AND" to Rhai "&&" and "OR" to Rhai "||"
let condition = condition.replace(" AND ", " && ").replace(" and ", " && ")
.replace(" OR ", " || ").replace(" or ", " || ");
// Convert BASIC "=" to Rhai "==" for comparisons in IF conditions
// Skip if it's already a comparison operator (==, !=, <=, >=) or assignment (+=, -=, etc.)
let condition = if !condition.contains("==") && !condition.contains("!=")
&& !condition.contains("<=") && !condition.contains(">=")
&& !condition.contains("+=") && !condition.contains("-=")
&& !condition.contains("*=") && !condition.contains("/=") {
condition.replace("=", "==")
} else {
condition.to_string()
};
log::info!("[TOOL] Converting IF statement: condition='{}'", condition);
result.push_str("if ");
result.push_str(&condition);
@ -681,6 +1050,31 @@ impl ScriptService {
continue;
}
// Handle ELSEIF ... THEN
if upper.starts_with("ELSEIF ") && upper.contains(" THEN") {
let then_pos = match upper.find(" THEN") {
Some(pos) => pos,
None => continue,
};
let condition = &trimmed[6..then_pos].trim();
let condition = condition.replace(" NOT IN ", " !in ").replace(" not in ", " !in ");
let condition = condition.replace(" AND ", " && ").replace(" and ", " && ")
.replace(" OR ", " || ").replace(" or ", " || ");
let condition = if !condition.contains("==") && !condition.contains("!=")
&& !condition.contains("<=") && !condition.contains(">=")
&& !condition.contains("+=") && !condition.contains("-=")
&& !condition.contains("*=") && !condition.contains("/=") {
condition.replace("=", "==")
} else {
condition.to_string()
};
log::info!("[TOOL] Converting ELSEIF statement: condition='{}'", condition);
result.push_str("} else if ");
result.push_str(&condition);
result.push_str(" {\n");
continue;
}
// Handle END IF
if upper == "END IF" {
log::info!("[TOOL] Converting END IF statement");
@ -709,6 +1103,85 @@ impl ScriptService {
continue;
}
// Handle BEGIN TALK ... END TALK (multi-line TALK with ${} substitution)
if upper == "BEGIN TALK" {
log::info!("[TOOL] Converting BEGIN TALK statement");
in_talk_block = true;
talk_block_lines.clear();
continue;
}
if upper == "END TALK" {
log::info!("[TOOL] Converting END TALK statement, processing {} lines", talk_block_lines.len());
in_talk_block = false;
// Split into multiple TALK statements to avoid expression complexity limit
// Use chunks of 5 lines per TALK statement
let chunk_size = 5;
for (chunk_idx, chunk) in talk_block_lines.chunks(chunk_size).enumerate() {
// Convert all talk lines in this chunk to a single TALK statement
let mut combined_talk = String::new();
for (i, talk_line) in chunk.iter().enumerate() {
let converted = Self::convert_talk_line_with_substitution(talk_line);
// Remove "TALK " prefix from converted line if present
let line_content = if converted.starts_with("TALK ") {
converted[5..].trim().to_string()
} else {
converted
};
if i > 0 {
combined_talk.push_str(" + \"\\n\" + ");
}
combined_talk.push_str(&line_content);
}
// Generate TALK statement for this chunk
result.push_str("TALK ");
result.push_str(&combined_talk);
result.push_str(";\n");
}
talk_block_lines.clear();
continue;
}
// If we're in a TALK block, collect lines
if in_talk_block {
// Skip empty lines but preserve them as blank TALK statements if needed
talk_block_lines.push(trimmed.to_string());
continue;
}
// Handle BEGIN MAIL ... END MAIL (multi-line email with ${} substitution)
if upper.starts_with("BEGIN MAIL ") {
let recipient = &trimmed[11..].trim(); // Skip "BEGIN MAIL "
log::info!("[TOOL] Converting BEGIN MAIL statement: recipient='{}'", recipient);
mail_recipient = recipient.to_string();
in_mail_block = true;
mail_block_lines.clear();
continue;
}
if upper == "END MAIL" {
log::info!("[TOOL] Converting END MAIL statement, processing {} lines", mail_block_lines.len());
in_mail_block = false;
// Process the mail block and convert to SEND EMAIL
let converted = Self::convert_mail_block(&mail_recipient, &mail_block_lines);
result.push_str(&converted);
result.push('\n');
mail_recipient.clear();
mail_block_lines.clear();
continue;
}
// If we're in a MAIL block, collect lines
if in_mail_block {
mail_block_lines.push(trimmed.to_string());
continue;
}
// Inside a WITH block - convert property assignments (key = value → key: value)
if in_with_block {
// Check if this is a property assignment (identifier = value)
@ -728,21 +1201,32 @@ impl ScriptService {
result.push_str(" ");
}
// Handle SAVE table, object → INSERT table, object
// BASIC SAVE uses 2 parameters but Rhai SAVE needs 3
// INSERT uses 2 parameters which matches the BASIC syntax
// Handle SAVE table, field1, field2, ... → INSERT "table", #{field1: value1, field2: value2, ...}
if upper.starts_with("SAVE") && upper.contains(',') {
log::info!("[TOOL] Processing SAVE line: '{}'", trimmed);
// Extract table and object name
// Extract the part after "SAVE"
let after_save = &trimmed[4..].trim(); // Skip "SAVE"
let parts: Vec<&str> = after_save.split(',').collect();
log::info!("[TOOL] SAVE parts: {:?}", parts);
if parts.len() == 2 {
if parts.len() >= 2 {
// First part is the table name (in quotes)
let table = parts[0].trim().trim_matches('"');
let object_name = parts[1].trim().trim_end_matches(';');
// Convert to INSERT table, object
let converted = format!("INSERT \"{}\", {};\n", table, object_name);
log::info!("[TOOL] Converted SAVE to INSERT: '{}'", converted);
// For old WITH block syntax (parts.len() == 2), convert to INSERT with object name
if parts.len() == 2 {
let object_name = parts[1].trim().trim_end_matches(';');
let converted = format!("INSERT \"{}\", {};\n", table, object_name);
log::info!("[TOOL] Converted SAVE to INSERT (old syntax): '{}'", converted);
result.push_str(&converted);
continue;
}
// For modern direct field list syntax (parts.len() > 2), just pass values as-is
// The runtime SAVE handler will match them to database columns by position
let values = parts[1..].join(", ");
let converted = format!("SAVE \"{}\", {};\n", table, values);
log::info!("[TOOL] Keeping SAVE syntax (modern): '{}'", converted);
result.push_str(&converted);
continue;
}
@ -787,16 +1271,42 @@ impl ScriptService {
&& !trimmed.contains("*=")
&& !trimmed.contains("/=");
// Check for line continuation (BASIC uses comma at end of line)
let ends_with_comma = trimmed.ends_with(',');
// If we're in a line continuation and this is not a variable assignment or statement,
// it's likely a string literal continuation - quote it
let line_to_process = if in_line_continuation && !is_var_assignment
&& !trimmed.contains('=') && !trimmed.starts_with('"') && !upper.starts_with("IF ") {
// This is a string literal continuation - quote it and escape any inner quotes
let escaped = trimmed.replace('"', "\\\"");
format!("\"{}\\n\"", escaped)
} else {
trimmed.to_string()
};
if is_var_assignment {
// Add 'let' for variable declarations
result.push_str("let ");
// Add 'let' for variable declarations, but only if line doesn't already start with let/LET
let trimmed_lower = trimmed.to_lowercase();
if !trimmed_lower.starts_with("let ") {
result.push_str("let ");
}
}
result.push_str(trimmed);
result.push_str(&line_to_process);
// Add semicolon if line doesn't have one and doesn't end with { or }
if !trimmed.ends_with(';') && !trimmed.ends_with('{') && !trimmed.ends_with('}') {
// Skip adding semicolons to:
// - SELECT/CASE/END SELECT statements (they're converted to if-else later)
// - Lines ending with comma (BASIC line continuation)
// - Lines that are part of a continuation block (in_line_continuation is true)
if !trimmed.ends_with(';') && !trimmed.ends_with('{') && !trimmed.ends_with('}')
&& !upper.starts_with("SELECT ") && !upper.starts_with("CASE ") && upper != "END SELECT"
&& !ends_with_comma && !in_line_continuation {
result.push(';');
}
result.push('\n');
// Update line continuation state
in_line_continuation = ends_with_comma;
} else {
result.push_str(trimmed);
result.push('\n');
@ -804,18 +1314,23 @@ impl ScriptService {
}
log::info!("[TOOL] IF/THEN conversion complete, output has {} lines", result.lines().count());
// Convert BASIC <> (not equal) to Rhai != globally
let result = result.replace(" <> ", " != ");
result
}
/// Convert BASIC SELECT ... CASE / END SELECT to Rhai match expressions
/// Convert BASIC SELECT ... CASE / END SELECT to if-else chains
/// Transforms: SELECT var ... CASE "value" ... END SELECT
/// Into: match var { "value" => { ... } ... }
fn convert_select_case_syntax(script: &str) -> String {
/// Into: if var == "value" { ... } else if var == "value2" { ... }
/// Note: We use if-else instead of match because 'match' is a reserved keyword in Rhai
pub fn convert_select_case_syntax(script: &str) -> String {
let mut result = String::new();
let mut lines: Vec<&str> = script.lines().collect();
let mut i = 0;
log::info!("[TOOL] Converting SELECT/CASE syntax");
log::info!("[TOOL] Converting SELECT/CASE syntax to if-else chains");
while i < lines.len() {
let trimmed = lines[i].trim();
@ -827,43 +1342,77 @@ impl ScriptService {
let select_var = trimmed[7..].trim(); // Skip "SELECT "
log::info!("[TOOL] Converting SELECT statement for variable: '{}'", select_var);
// Start match expression
result.push_str(&format!("match {} {{\n", select_var));
// Skip the SELECT line
i += 1;
// Process CASE statements until END SELECT
let mut current_case_body: Vec<String> = Vec::new();
let mut in_case = false;
let mut is_first_case = true;
while i < lines.len() {
let case_trimmed = lines[i].trim();
let case_upper = case_trimmed.to_uppercase();
// Skip empty lines and comment lines within SELECT/CASE blocks
if case_trimmed.is_empty() || case_trimmed.starts_with('\'') || case_trimmed.starts_with('#') {
i += 1;
continue;
}
if case_upper == "END SELECT" {
// Close any open case
if in_case {
for body_line in &current_case_body {
result.push_str(" ");
result.push_str(body_line);
// Add semicolon if line doesn't have one
if !body_line.ends_with(';') && !body_line.ends_with('{') && !body_line.ends_with('}') {
result.push(';');
}
result.push('\n');
}
// Close the last case arm (no else if, so we need the closing brace)
result.push_str(" }\n");
current_case_body.clear();
in_case = false;
}
// Close the match expression
result.push_str("}\n");
// No extra closing brace needed - the last } else if ... { already closed the chain
i += 1;
break;
} else if case_upper.starts_with("CASE ") {
// Close previous case if any
} else if case_upper.starts_with("SELECT ") {
// Encountered another SELECT statement while processing this SELECT block
// Close the current if-else chain and break to let the outer loop handle the new SELECT
if in_case {
for body_line in &current_case_body {
result.push_str(" ");
result.push_str(body_line);
// Add semicolon if line doesn't have one
if !body_line.ends_with(';') && !body_line.ends_with('{') && !body_line.ends_with('}') {
result.push(';');
}
result.push('\n');
}
// Close the current case arm (no else if, so we need the closing brace)
result.push_str(" }\n");
current_case_body.clear();
in_case = false;
}
// No extra closing brace needed
break;
} else if case_upper.starts_with("CASE ") {
// Close previous case if any (but NOT if we're about to start else if)
if in_case {
for body_line in &current_case_body {
result.push_str(" ");
result.push_str(body_line);
// Add semicolon if line doesn't have one
if !body_line.ends_with(';') && !body_line.ends_with('{') && !body_line.ends_with('}') {
result.push(';');
}
result.push('\n');
}
// NOTE: Don't close the case arm here - the } else if will close it
current_case_body.clear();
}
@ -876,14 +1425,22 @@ impl ScriptService {
format!("\"{}\"", case_trimmed[5..].trim())
};
result.push_str(&format!(" {} => {{\n", case_value));
// Start if/else if chain
if is_first_case {
result.push_str(&format!("if {} == {} {{\n", select_var, case_value));
is_first_case = false;
} else {
result.push_str(&format!("}} else if {} == {} {{\n", select_var, case_value));
}
in_case = true;
i += 1;
} else {
} else if in_case {
// Collect body lines for the current case
if in_case {
current_case_body.push(lines[i].to_string());
}
current_case_body.push(lines[i].to_string());
i += 1;
} else {
// We're in the SELECT block but not in a CASE yet
// Skip this line and move to the next
i += 1;
}
}
@ -892,9 +1449,11 @@ impl ScriptService {
}
// Not a SELECT statement - just copy the line
result.push_str(lines[i]);
result.push('\n');
i += 1;
if i < lines.len() {
result.push_str(lines[i]);
result.push('\n');
i += 1;
}
}
result
@ -902,7 +1461,7 @@ impl ScriptService {
/// Convert BASIC keywords to lowercase without touching variables
/// This is a simplified version of normalize_variables_to_lowercase for tools
fn convert_keywords_to_lowercase(script: &str) -> String {
pub fn convert_keywords_to_lowercase(script: &str) -> String {
let keywords = [
"IF", "THEN", "ELSE", "END IF", "FOR", "NEXT", "WHILE", "WEND",
"DO", "LOOP", "RETURN", "EXIT",
@ -1238,7 +1797,7 @@ impl ScriptService {
/// - "USE WEBSITE "url" REFRESH "interval"" → "USE_WEBSITE("url", "interval")"
/// - "SET BOT MEMORY key AS value" → "SET_BOT_MEMORY(key, value)"
/// - "CLEAR SUGGESTIONS" → "CLEAR_SUGGESTIONS()"
fn convert_multiword_keywords(script: &str) -> String {
pub fn convert_multiword_keywords(script: &str) -> String {
use regex::Regex;
// Known multi-word keywords with their conversion patterns