diff --git a/src/basic/mod.rs b/src/basic/mod.rs index 05cd9b4c5..b7b703dc0 100644 --- a/src/basic/mod.rs +++ b/src/basic/mod.rs @@ -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::>() @@ -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(¤t_expr); + } else { + result.push_str(" + "); + result.push_str(¤t_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 = 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 = 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(¤t_literal.replace('"', "\\\"")); + result.push('"'); + } else { + result.push_str(" + \""); + result.push_str(¤t_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(¤t_var); + } else { + result.push_str(" + "); + result.push_str(¤t_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(¤t_literal.replace('"', "\\\"")); + result.push('"'); + } else { + result.push_str(" + \""); + result.push_str(¤t_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 = Vec::new(); let mut in_with_block = false; + let mut in_talk_block = false; + let mut talk_block_lines: Vec = Vec::new(); + let mut in_mail_block = false; + let mut mail_recipient = String::new(); + let mut mail_block_lines: Vec = 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 = 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 ¤t_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 ¤t_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 ¤t_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