- Add a new crate `sal-text` for text manipulation utilities. - Integrate `sal-text` into the main `sal` crate. - Remove the previous `text` module from `sal`. This improves organization and allows for independent development of the `sal-text` library.
This commit is contained in:
		
							
								
								
									
										137
									
								
								text/src/dedent.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								text/src/dedent.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,137 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Dedent a multiline string by removing common leading whitespace.
 | 
			
		||||
 *
 | 
			
		||||
 * This function analyzes all non-empty lines in the input text to determine
 | 
			
		||||
 * the minimum indentation level, then removes that amount of whitespace
 | 
			
		||||
 * from the beginning of each line. This is useful for working with
 | 
			
		||||
 * multi-line strings in code that have been indented to match the
 | 
			
		||||
 * surrounding code structure.
 | 
			
		||||
 *
 | 
			
		||||
 * # Arguments
 | 
			
		||||
 *
 | 
			
		||||
 * * `text` - The multiline string to dedent
 | 
			
		||||
 *
 | 
			
		||||
 * # Returns
 | 
			
		||||
 *
 | 
			
		||||
 * * `String` - The dedented string
 | 
			
		||||
 *
 | 
			
		||||
 * # Examples
 | 
			
		||||
 *
 | 
			
		||||
 * ```
 | 
			
		||||
 * use sal::text::dedent;
 | 
			
		||||
 *
 | 
			
		||||
 * let indented = "    line 1\n    line 2\n        line 3";
 | 
			
		||||
 * let dedented = dedent(indented);
 | 
			
		||||
 * assert_eq!(dedented, "line 1\nline 2\n    line 3");
 | 
			
		||||
 * ```
 | 
			
		||||
 *
 | 
			
		||||
 * # Notes
 | 
			
		||||
 *
 | 
			
		||||
 * - Empty lines are preserved but have all leading whitespace removed
 | 
			
		||||
 * - Tabs are counted as 4 spaces for indentation purposes
 | 
			
		||||
 */
 | 
			
		||||
pub fn dedent(text: &str) -> String {
 | 
			
		||||
    let lines: Vec<&str> = text.lines().collect();
 | 
			
		||||
 | 
			
		||||
    // Find the minimum indentation level (ignore empty lines)
 | 
			
		||||
    let min_indent = lines
 | 
			
		||||
        .iter()
 | 
			
		||||
        .filter(|line| !line.trim().is_empty())
 | 
			
		||||
        .map(|line| {
 | 
			
		||||
            let mut spaces = 0;
 | 
			
		||||
            for c in line.chars() {
 | 
			
		||||
                if c == ' ' {
 | 
			
		||||
                    spaces += 1;
 | 
			
		||||
                } else if c == '\t' {
 | 
			
		||||
                    spaces += 4; // Count tabs as 4 spaces
 | 
			
		||||
                } else {
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            spaces
 | 
			
		||||
        })
 | 
			
		||||
        .min()
 | 
			
		||||
        .unwrap_or(0);
 | 
			
		||||
 | 
			
		||||
    // Remove that many spaces from the beginning of each line
 | 
			
		||||
    lines
 | 
			
		||||
        .iter()
 | 
			
		||||
        .map(|line| {
 | 
			
		||||
            if line.trim().is_empty() {
 | 
			
		||||
                return String::new();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let mut count = 0;
 | 
			
		||||
            let mut chars = line.chars().peekable();
 | 
			
		||||
 | 
			
		||||
            // Skip initial spaces up to min_indent
 | 
			
		||||
            while count < min_indent && chars.peek().is_some() {
 | 
			
		||||
                match chars.peek() {
 | 
			
		||||
                    Some(' ') => {
 | 
			
		||||
                        chars.next();
 | 
			
		||||
                        count += 1;
 | 
			
		||||
                    }
 | 
			
		||||
                    Some('\t') => {
 | 
			
		||||
                        chars.next();
 | 
			
		||||
                        count += 4;
 | 
			
		||||
                    }
 | 
			
		||||
                    _ => break,
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Return the remaining characters
 | 
			
		||||
            chars.collect::<String>()
 | 
			
		||||
        })
 | 
			
		||||
        .collect::<Vec<String>>()
 | 
			
		||||
        .join("\n")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Prefix a multiline string with a specified prefix.
 | 
			
		||||
 *
 | 
			
		||||
 * This function adds the specified prefix to the beginning of each line in the input text.
 | 
			
		||||
 *
 | 
			
		||||
 * # Arguments
 | 
			
		||||
 *
 | 
			
		||||
 * * `text` - The multiline string to prefix
 | 
			
		||||
 * * `prefix` - The prefix to add to each line
 | 
			
		||||
 *
 | 
			
		||||
 * # Returns
 | 
			
		||||
 *
 | 
			
		||||
 * * `String` - The prefixed string
 | 
			
		||||
 *
 | 
			
		||||
 * # Examples
 | 
			
		||||
 *
 | 
			
		||||
 * ```
 | 
			
		||||
 * use sal::text::prefix;
 | 
			
		||||
 *
 | 
			
		||||
 * let text = "line 1\nline 2\nline 3";
 | 
			
		||||
 * let prefixed = prefix(text, "    ");
 | 
			
		||||
 * assert_eq!(prefixed, "    line 1\n    line 2\n    line 3");
 | 
			
		||||
 * ```
 | 
			
		||||
 */
 | 
			
		||||
pub fn prefix(text: &str, prefix: &str) -> String {
 | 
			
		||||
    text.lines()
 | 
			
		||||
        .map(|line| format!("{}{}", prefix, line))
 | 
			
		||||
        .collect::<Vec<String>>()
 | 
			
		||||
        .join("\n")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod tests {
 | 
			
		||||
    use super::*;
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_dedent() {
 | 
			
		||||
        let indented = "    line 1\n    line 2\n        line 3";
 | 
			
		||||
        let dedented = dedent(indented);
 | 
			
		||||
        assert_eq!(dedented, "line 1\nline 2\n    line 3");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_prefix() {
 | 
			
		||||
        let text = "line 1\nline 2\nline 3";
 | 
			
		||||
        let prefixed = prefix(text, "    ");
 | 
			
		||||
        assert_eq!(prefixed, "    line 1\n    line 2\n    line 3");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										99
									
								
								text/src/fix.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								text/src/fix.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,99 @@
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
pub fn name_fix(text: &str) -> String {
 | 
			
		||||
    let mut result = String::with_capacity(text.len());
 | 
			
		||||
    
 | 
			
		||||
    let mut last_was_underscore = false;
 | 
			
		||||
    for c in text.chars() {
 | 
			
		||||
        // Keep only ASCII characters
 | 
			
		||||
        if c.is_ascii() {
 | 
			
		||||
            // Replace specific characters with underscore
 | 
			
		||||
            if c.is_whitespace() || c == ',' || c == '-' || c == '"' || c == '\'' ||
 | 
			
		||||
               c == '#' || c == '!' || c == '(' || c == ')' || c == '[' || c == ']' ||
 | 
			
		||||
               c == '=' || c == '+' || c == '<' || c == '>' || c == '@' || c == '$' ||
 | 
			
		||||
               c == '%' || c == '^' || c == '&' || c == '*' {
 | 
			
		||||
                // Only add underscore if the last character wasn't an underscore
 | 
			
		||||
                if !last_was_underscore {
 | 
			
		||||
                    result.push('_');
 | 
			
		||||
                    last_was_underscore = true;
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                // Add the character as is (will be converted to lowercase later)
 | 
			
		||||
                result.push(c);
 | 
			
		||||
                last_was_underscore = false;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        // Non-ASCII characters are simply skipped
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Convert to lowercase
 | 
			
		||||
    return result.to_lowercase();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn path_fix(text: &str) -> String {
 | 
			
		||||
    // If path ends with '/', return as is
 | 
			
		||||
    if text.ends_with('/') {
 | 
			
		||||
        return text.to_string();
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Find the last '/' to extract the filename part
 | 
			
		||||
    match text.rfind('/') {
 | 
			
		||||
        Some(pos) => {
 | 
			
		||||
            // Extract the path and filename parts
 | 
			
		||||
            let path = &text[..=pos];
 | 
			
		||||
            let filename = &text[pos+1..];
 | 
			
		||||
            
 | 
			
		||||
            // Apply name_fix to the filename part only
 | 
			
		||||
            return format!("{}{}", path, name_fix(filename));
 | 
			
		||||
        },
 | 
			
		||||
        None => {
 | 
			
		||||
            // No '/' found, so the entire text is a filename
 | 
			
		||||
            return name_fix(text);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod tests {
 | 
			
		||||
    use super::*;
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_name_fix() {
 | 
			
		||||
        // Test ASCII conversion and special character replacement
 | 
			
		||||
        assert_eq!(name_fix("Hello World"), "hello_world");
 | 
			
		||||
        assert_eq!(name_fix("File-Name.txt"), "file_name.txt");
 | 
			
		||||
        assert_eq!(name_fix("Test!@#$%^&*()"), "test_");
 | 
			
		||||
        assert_eq!(name_fix("Space, Tab\t, Comma,"), "space_tab_comma_");
 | 
			
		||||
        assert_eq!(name_fix("Quotes\"'"), "quotes_");
 | 
			
		||||
        assert_eq!(name_fix("Brackets[]<>"), "brackets_");
 | 
			
		||||
        assert_eq!(name_fix("Operators=+-"), "operators_");
 | 
			
		||||
        
 | 
			
		||||
        // Test non-ASCII characters removal
 | 
			
		||||
        assert_eq!(name_fix("Café"), "caf");
 | 
			
		||||
        assert_eq!(name_fix("Résumé"), "rsum");
 | 
			
		||||
        assert_eq!(name_fix("Über"), "ber");
 | 
			
		||||
        
 | 
			
		||||
        // Test lowercase conversion
 | 
			
		||||
        assert_eq!(name_fix("UPPERCASE"), "uppercase");
 | 
			
		||||
        assert_eq!(name_fix("MixedCase"), "mixedcase");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_path_fix() {
 | 
			
		||||
        // Test path ending with /
 | 
			
		||||
        assert_eq!(path_fix("/path/to/directory/"), "/path/to/directory/");
 | 
			
		||||
        
 | 
			
		||||
        // Test single filename
 | 
			
		||||
        assert_eq!(path_fix("filename.txt"), "filename.txt");
 | 
			
		||||
        assert_eq!(path_fix("UPPER-file.md"), "upper_file.md");
 | 
			
		||||
        
 | 
			
		||||
        // Test path with filename
 | 
			
		||||
        assert_eq!(path_fix("/path/to/File Name.txt"), "/path/to/file_name.txt");
 | 
			
		||||
        assert_eq!(path_fix("./relative/path/to/DOCUMENT-123.pdf"), "./relative/path/to/document_123.pdf");
 | 
			
		||||
        assert_eq!(path_fix("/absolute/path/to/Résumé.doc"), "/absolute/path/to/rsum.doc");
 | 
			
		||||
        
 | 
			
		||||
        // Test path with special characters in filename
 | 
			
		||||
        assert_eq!(path_fix("/path/with/[special]<chars>.txt"), "/path/with/_special_chars_.txt");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										59
									
								
								text/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								text/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,59 @@
 | 
			
		||||
//! SAL Text - Text processing and manipulation utilities
 | 
			
		||||
//!
 | 
			
		||||
//! This crate provides a comprehensive collection of text processing utilities including:
 | 
			
		||||
//! - **Text indentation**: Remove common leading whitespace (`dedent`) and add prefixes (`prefix`)
 | 
			
		||||
//! - **String normalization**: Sanitize strings for filenames (`name_fix`) and paths (`path_fix`)
 | 
			
		||||
//! - **Text replacement**: Powerful `TextReplacer` for regex and literal replacements
 | 
			
		||||
//! - **Template rendering**: `TemplateBuilder` using Tera engine for dynamic text generation
 | 
			
		||||
//!
 | 
			
		||||
//! All functionality is available in both Rust and Rhai scripting environments.
 | 
			
		||||
//!
 | 
			
		||||
//! # Examples
 | 
			
		||||
//!
 | 
			
		||||
//! ## Text Indentation
 | 
			
		||||
//!
 | 
			
		||||
//! ```rust
 | 
			
		||||
//! use sal_text::dedent;
 | 
			
		||||
//!
 | 
			
		||||
//! let indented = "    line 1\n    line 2\n        line 3";
 | 
			
		||||
//! let dedented = dedent(indented);
 | 
			
		||||
//! assert_eq!(dedented, "line 1\nline 2\n    line 3");
 | 
			
		||||
//! ```
 | 
			
		||||
//!
 | 
			
		||||
//! ## String Normalization
 | 
			
		||||
//!
 | 
			
		||||
//! ```rust
 | 
			
		||||
//! use sal_text::name_fix;
 | 
			
		||||
//!
 | 
			
		||||
//! let unsafe_name = "User's File [Draft].txt";
 | 
			
		||||
//! let safe_name = name_fix(unsafe_name);
 | 
			
		||||
//! assert_eq!(safe_name, "users_file_draft_.txt");
 | 
			
		||||
//! ```
 | 
			
		||||
//!
 | 
			
		||||
//! ## Text Replacement
 | 
			
		||||
//!
 | 
			
		||||
//! ```rust
 | 
			
		||||
//! use sal_text::TextReplacer;
 | 
			
		||||
//!
 | 
			
		||||
//! let replacer = TextReplacer::builder()
 | 
			
		||||
//!     .pattern(r"\d+")
 | 
			
		||||
//!     .replacement("NUMBER")
 | 
			
		||||
//!     .regex(true)
 | 
			
		||||
//!     .build()
 | 
			
		||||
//!     .expect("Failed to build replacer");
 | 
			
		||||
//!
 | 
			
		||||
//! let result = replacer.replace("There are 123 items");
 | 
			
		||||
//! assert_eq!(result, "There are NUMBER items");
 | 
			
		||||
//! ```
 | 
			
		||||
 | 
			
		||||
mod dedent;
 | 
			
		||||
mod fix;
 | 
			
		||||
mod replace;
 | 
			
		||||
mod template;
 | 
			
		||||
 | 
			
		||||
pub mod rhai;
 | 
			
		||||
 | 
			
		||||
pub use dedent::*;
 | 
			
		||||
pub use fix::*;
 | 
			
		||||
pub use replace::*;
 | 
			
		||||
pub use template::*;
 | 
			
		||||
							
								
								
									
										292
									
								
								text/src/replace.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										292
									
								
								text/src/replace.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,292 @@
 | 
			
		||||
use regex::Regex;
 | 
			
		||||
use std::fs;
 | 
			
		||||
use std::io::{self, Read};
 | 
			
		||||
use std::path::Path;
 | 
			
		||||
 | 
			
		||||
/// Represents the type of replacement to perform.
 | 
			
		||||
#[derive(Clone)]
 | 
			
		||||
pub enum ReplaceMode {
 | 
			
		||||
    /// Regex-based replacement using the `regex` crate
 | 
			
		||||
    Regex(Regex),
 | 
			
		||||
    /// Literal substring replacement (non-regex)
 | 
			
		||||
    Literal(String),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// A single replacement operation with a pattern and replacement text
 | 
			
		||||
#[derive(Clone)]
 | 
			
		||||
pub struct ReplacementOperation {
 | 
			
		||||
    mode: ReplaceMode,
 | 
			
		||||
    replacement: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ReplacementOperation {
 | 
			
		||||
    /// Applies this replacement operation to the input text
 | 
			
		||||
    fn apply(&self, input: &str) -> String {
 | 
			
		||||
        match &self.mode {
 | 
			
		||||
            ReplaceMode::Regex(re) => re.replace_all(input, self.replacement.as_str()).to_string(),
 | 
			
		||||
            ReplaceMode::Literal(search) => input.replace(search, &self.replacement),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Text replacer that can perform multiple replacement operations
 | 
			
		||||
/// in a single pass over the input text.
 | 
			
		||||
#[derive(Clone)]
 | 
			
		||||
pub struct TextReplacer {
 | 
			
		||||
    operations: Vec<ReplacementOperation>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl TextReplacer {
 | 
			
		||||
    /// Creates a new builder for configuring a TextReplacer
 | 
			
		||||
    pub fn builder() -> TextReplacerBuilder {
 | 
			
		||||
        TextReplacerBuilder::default()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Applies all configured replacement operations to the input text
 | 
			
		||||
    pub fn replace(&self, input: &str) -> String {
 | 
			
		||||
        let mut result = input.to_string();
 | 
			
		||||
 | 
			
		||||
        // Apply each replacement operation in sequence
 | 
			
		||||
        for op in &self.operations {
 | 
			
		||||
            result = op.apply(&result);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        result
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Reads a file, applies all replacements, and returns the result as a string
 | 
			
		||||
    pub fn replace_file<P: AsRef<Path>>(&self, path: P) -> io::Result<String> {
 | 
			
		||||
        let mut file = fs::File::open(path)?;
 | 
			
		||||
        let mut content = String::new();
 | 
			
		||||
        file.read_to_string(&mut content)?;
 | 
			
		||||
 | 
			
		||||
        Ok(self.replace(&content))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Reads a file, applies all replacements, and writes the result back to the file
 | 
			
		||||
    pub fn replace_file_in_place<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
 | 
			
		||||
        let content = self.replace_file(&path)?;
 | 
			
		||||
        fs::write(path, content)?;
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Reads a file, applies all replacements, and writes the result to a new file
 | 
			
		||||
    pub fn replace_file_to<P1: AsRef<Path>, P2: AsRef<Path>>(
 | 
			
		||||
        &self,
 | 
			
		||||
        input_path: P1,
 | 
			
		||||
        output_path: P2,
 | 
			
		||||
    ) -> io::Result<()> {
 | 
			
		||||
        let content = self.replace_file(&input_path)?;
 | 
			
		||||
        fs::write(output_path, content)?;
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Builder for the TextReplacer.
 | 
			
		||||
#[derive(Default, Clone)]
 | 
			
		||||
pub struct TextReplacerBuilder {
 | 
			
		||||
    operations: Vec<ReplacementOperation>,
 | 
			
		||||
    pattern: Option<String>,
 | 
			
		||||
    replacement: Option<String>,
 | 
			
		||||
    use_regex: bool,
 | 
			
		||||
    case_insensitive: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl TextReplacerBuilder {
 | 
			
		||||
    /// Sets the pattern to search for
 | 
			
		||||
    pub fn pattern(mut self, pat: &str) -> Self {
 | 
			
		||||
        self.pattern = Some(pat.to_string());
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Sets the replacement text
 | 
			
		||||
    pub fn replacement(mut self, rep: &str) -> Self {
 | 
			
		||||
        self.replacement = Some(rep.to_string());
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Sets whether to use regex
 | 
			
		||||
    pub fn regex(mut self, yes: bool) -> Self {
 | 
			
		||||
        self.use_regex = yes;
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Sets whether the replacement should be case-insensitive
 | 
			
		||||
    pub fn case_insensitive(mut self, yes: bool) -> Self {
 | 
			
		||||
        self.case_insensitive = yes;
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Adds another replacement operation to the chain and resets the builder for a new operation
 | 
			
		||||
    pub fn and(mut self) -> Self {
 | 
			
		||||
        self.add_current_operation();
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Helper method to add the current operation to the list
 | 
			
		||||
    fn add_current_operation(&mut self) -> bool {
 | 
			
		||||
        if let Some(pattern) = self.pattern.take() {
 | 
			
		||||
            let replacement = self.replacement.take().unwrap_or_default();
 | 
			
		||||
            let use_regex = self.use_regex;
 | 
			
		||||
            let case_insensitive = self.case_insensitive;
 | 
			
		||||
 | 
			
		||||
            // Reset current settings
 | 
			
		||||
            self.use_regex = false;
 | 
			
		||||
            self.case_insensitive = false;
 | 
			
		||||
 | 
			
		||||
            // Create the replacement mode
 | 
			
		||||
            let mode = if use_regex {
 | 
			
		||||
                let mut regex_pattern = pattern;
 | 
			
		||||
 | 
			
		||||
                // If case insensitive, add the flag to the regex pattern
 | 
			
		||||
                if case_insensitive && !regex_pattern.starts_with("(?i)") {
 | 
			
		||||
                    regex_pattern = format!("(?i){}", regex_pattern);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                match Regex::new(®ex_pattern) {
 | 
			
		||||
                    Ok(re) => ReplaceMode::Regex(re),
 | 
			
		||||
                    Err(_) => return false, // Failed to compile regex
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                // For literal replacement, we'll handle case insensitivity differently
 | 
			
		||||
                // since String::replace doesn't have a case-insensitive option
 | 
			
		||||
                if case_insensitive {
 | 
			
		||||
                    return false; // Case insensitive not supported for literal
 | 
			
		||||
                }
 | 
			
		||||
                ReplaceMode::Literal(pattern)
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            self.operations
 | 
			
		||||
                .push(ReplacementOperation { mode, replacement });
 | 
			
		||||
 | 
			
		||||
            true
 | 
			
		||||
        } else {
 | 
			
		||||
            false
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Builds the TextReplacer with all configured replacement operations
 | 
			
		||||
    pub fn build(mut self) -> Result<TextReplacer, String> {
 | 
			
		||||
        // If there's a pending replacement operation, add it
 | 
			
		||||
        self.add_current_operation();
 | 
			
		||||
 | 
			
		||||
        // Ensure we have at least one replacement operation
 | 
			
		||||
        if self.operations.is_empty() {
 | 
			
		||||
            return Err("No replacement operations configured".to_string());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(TextReplacer {
 | 
			
		||||
            operations: self.operations,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod tests {
 | 
			
		||||
    use super::*;
 | 
			
		||||
    use std::io::{Seek, SeekFrom, Write};
 | 
			
		||||
    use tempfile::NamedTempFile;
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_regex_replace() {
 | 
			
		||||
        let replacer = TextReplacer::builder()
 | 
			
		||||
            .pattern(r"\bfoo\b")
 | 
			
		||||
            .replacement("bar")
 | 
			
		||||
            .regex(true)
 | 
			
		||||
            .build()
 | 
			
		||||
            .unwrap();
 | 
			
		||||
 | 
			
		||||
        let input = "foo bar foo baz";
 | 
			
		||||
        let output = replacer.replace(input);
 | 
			
		||||
 | 
			
		||||
        assert_eq!(output, "bar bar bar baz");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_literal_replace() {
 | 
			
		||||
        let replacer = TextReplacer::builder()
 | 
			
		||||
            .pattern("foo")
 | 
			
		||||
            .replacement("qux")
 | 
			
		||||
            .regex(false)
 | 
			
		||||
            .build()
 | 
			
		||||
            .unwrap();
 | 
			
		||||
 | 
			
		||||
        let input = "foo bar foo baz";
 | 
			
		||||
        let output = replacer.replace(input);
 | 
			
		||||
 | 
			
		||||
        assert_eq!(output, "qux bar qux baz");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_multiple_replacements() {
 | 
			
		||||
        let replacer = TextReplacer::builder()
 | 
			
		||||
            .pattern("foo")
 | 
			
		||||
            .replacement("qux")
 | 
			
		||||
            .and()
 | 
			
		||||
            .pattern("bar")
 | 
			
		||||
            .replacement("baz")
 | 
			
		||||
            .build()
 | 
			
		||||
            .unwrap();
 | 
			
		||||
 | 
			
		||||
        let input = "foo bar foo";
 | 
			
		||||
        let output = replacer.replace(input);
 | 
			
		||||
 | 
			
		||||
        assert_eq!(output, "qux baz qux");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_case_insensitive_regex() {
 | 
			
		||||
        let replacer = TextReplacer::builder()
 | 
			
		||||
            .pattern("foo")
 | 
			
		||||
            .replacement("bar")
 | 
			
		||||
            .regex(true)
 | 
			
		||||
            .case_insensitive(true)
 | 
			
		||||
            .build()
 | 
			
		||||
            .unwrap();
 | 
			
		||||
 | 
			
		||||
        let input = "FOO foo Foo";
 | 
			
		||||
        let output = replacer.replace(input);
 | 
			
		||||
 | 
			
		||||
        assert_eq!(output, "bar bar bar");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_file_operations() -> io::Result<()> {
 | 
			
		||||
        // Create a temporary file
 | 
			
		||||
        let mut temp_file = NamedTempFile::new()?;
 | 
			
		||||
        writeln!(temp_file, "foo bar foo baz")?;
 | 
			
		||||
 | 
			
		||||
        // Flush the file to ensure content is written
 | 
			
		||||
        temp_file.as_file_mut().flush()?;
 | 
			
		||||
 | 
			
		||||
        let replacer = TextReplacer::builder()
 | 
			
		||||
            .pattern("foo")
 | 
			
		||||
            .replacement("qux")
 | 
			
		||||
            .build()
 | 
			
		||||
            .unwrap();
 | 
			
		||||
 | 
			
		||||
        // Test replace_file
 | 
			
		||||
        let result = replacer.replace_file(temp_file.path())?;
 | 
			
		||||
        assert_eq!(result, "qux bar qux baz\n");
 | 
			
		||||
 | 
			
		||||
        // Test replace_file_in_place
 | 
			
		||||
        replacer.replace_file_in_place(temp_file.path())?;
 | 
			
		||||
 | 
			
		||||
        // Verify the file was updated - need to seek to beginning of file first
 | 
			
		||||
        let mut content = String::new();
 | 
			
		||||
        temp_file.as_file_mut().seek(SeekFrom::Start(0))?;
 | 
			
		||||
        temp_file.as_file_mut().read_to_string(&mut content)?;
 | 
			
		||||
        assert_eq!(content, "qux bar qux baz\n");
 | 
			
		||||
 | 
			
		||||
        // Test replace_file_to with a new temporary file
 | 
			
		||||
        let output_file = NamedTempFile::new()?;
 | 
			
		||||
        replacer.replace_file_to(temp_file.path(), output_file.path())?;
 | 
			
		||||
 | 
			
		||||
        // Verify the output file has the replaced content
 | 
			
		||||
        let mut output_content = String::new();
 | 
			
		||||
        fs::File::open(output_file.path())?.read_to_string(&mut output_content)?;
 | 
			
		||||
        assert_eq!(output_content, "qux bar qux baz\n");
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										228
									
								
								text/src/rhai.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										228
									
								
								text/src/rhai.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,228 @@
 | 
			
		||||
//! Rhai wrappers for Text module functions
 | 
			
		||||
//!
 | 
			
		||||
//! This module provides Rhai wrappers for the functions in the Text module.
 | 
			
		||||
 | 
			
		||||
use crate::{TemplateBuilder, TextReplacer, TextReplacerBuilder};
 | 
			
		||||
use rhai::{Array, Engine, EvalAltResult, Map, Position};
 | 
			
		||||
use std::collections::HashMap;
 | 
			
		||||
 | 
			
		||||
/// Register Text module functions with the Rhai engine
 | 
			
		||||
///
 | 
			
		||||
/// # Arguments
 | 
			
		||||
///
 | 
			
		||||
/// * `engine` - The Rhai engine to register the functions with
 | 
			
		||||
///
 | 
			
		||||
/// # Returns
 | 
			
		||||
///
 | 
			
		||||
/// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise
 | 
			
		||||
pub fn register_text_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
 | 
			
		||||
    // Register types
 | 
			
		||||
    register_text_types(engine)?;
 | 
			
		||||
 | 
			
		||||
    // Register TextReplacer constructor
 | 
			
		||||
    engine.register_fn("text_replacer_new", text_replacer_new);
 | 
			
		||||
 | 
			
		||||
    // Register TextReplacerBuilder instance methods
 | 
			
		||||
    engine.register_fn("pattern", pattern);
 | 
			
		||||
    engine.register_fn("replacement", replacement);
 | 
			
		||||
    engine.register_fn("regex", regex);
 | 
			
		||||
    engine.register_fn("case_insensitive", case_insensitive);
 | 
			
		||||
    engine.register_fn("and", and);
 | 
			
		||||
    engine.register_fn("build", build);
 | 
			
		||||
 | 
			
		||||
    // Register TextReplacer instance methods
 | 
			
		||||
    engine.register_fn("replace", replace);
 | 
			
		||||
    engine.register_fn("replace_file", replace_file);
 | 
			
		||||
    engine.register_fn("replace_file_in_place", replace_file_in_place);
 | 
			
		||||
    engine.register_fn("replace_file_to", replace_file_to);
 | 
			
		||||
 | 
			
		||||
    // Register TemplateBuilder constructor
 | 
			
		||||
    engine.register_fn("template_builder_open", template_builder_open);
 | 
			
		||||
 | 
			
		||||
    // Register TemplateBuilder instance methods
 | 
			
		||||
    engine.register_fn("add_var", add_var_string);
 | 
			
		||||
    engine.register_fn("add_var", add_var_int);
 | 
			
		||||
    engine.register_fn("add_var", add_var_float);
 | 
			
		||||
    engine.register_fn("add_var", add_var_bool);
 | 
			
		||||
    engine.register_fn("add_var", add_var_array);
 | 
			
		||||
    engine.register_fn("add_vars", add_vars);
 | 
			
		||||
    engine.register_fn("render", render);
 | 
			
		||||
    engine.register_fn("render_to_file", render_to_file);
 | 
			
		||||
 | 
			
		||||
    // Register Fix functions directly from text module
 | 
			
		||||
    engine.register_fn("name_fix", crate::name_fix);
 | 
			
		||||
    engine.register_fn("path_fix", crate::path_fix);
 | 
			
		||||
 | 
			
		||||
    // Register Dedent functions directly from text module
 | 
			
		||||
    engine.register_fn("dedent", crate::dedent);
 | 
			
		||||
    engine.register_fn("prefix", crate::prefix);
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Register Text module types with the Rhai engine
 | 
			
		||||
fn register_text_types(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
 | 
			
		||||
    // Register TextReplacerBuilder type
 | 
			
		||||
    engine.register_type_with_name::<TextReplacerBuilder>("TextReplacerBuilder");
 | 
			
		||||
 | 
			
		||||
    // Register TextReplacer type
 | 
			
		||||
    engine.register_type_with_name::<TextReplacer>("TextReplacer");
 | 
			
		||||
 | 
			
		||||
    // Register TemplateBuilder type
 | 
			
		||||
    engine.register_type_with_name::<TemplateBuilder>("TemplateBuilder");
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Helper functions for error conversion
 | 
			
		||||
fn io_error_to_rhai_error<T>(result: std::io::Result<T>) -> Result<T, Box<EvalAltResult>> {
 | 
			
		||||
    result.map_err(|e| {
 | 
			
		||||
        Box::new(EvalAltResult::ErrorRuntime(
 | 
			
		||||
            format!("IO error: {}", e).into(),
 | 
			
		||||
            Position::NONE,
 | 
			
		||||
        ))
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn tera_error_to_rhai_error<T>(result: Result<T, tera::Error>) -> Result<T, Box<EvalAltResult>> {
 | 
			
		||||
    result.map_err(|e| {
 | 
			
		||||
        Box::new(EvalAltResult::ErrorRuntime(
 | 
			
		||||
            format!("Template error: {}", e).into(),
 | 
			
		||||
            Position::NONE,
 | 
			
		||||
        ))
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn string_error_to_rhai_error<T>(result: Result<T, String>) -> Result<T, Box<EvalAltResult>> {
 | 
			
		||||
    result.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(e.into(), Position::NONE)))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TextReplacer implementation
 | 
			
		||||
 | 
			
		||||
/// Creates a new TextReplacerBuilder
 | 
			
		||||
pub fn text_replacer_new() -> TextReplacerBuilder {
 | 
			
		||||
    TextReplacerBuilder::default()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Sets the pattern to search for
 | 
			
		||||
pub fn pattern(builder: TextReplacerBuilder, pat: &str) -> TextReplacerBuilder {
 | 
			
		||||
    builder.pattern(pat)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Sets the replacement text
 | 
			
		||||
pub fn replacement(builder: TextReplacerBuilder, rep: &str) -> TextReplacerBuilder {
 | 
			
		||||
    builder.replacement(rep)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Sets whether to use regex
 | 
			
		||||
pub fn regex(builder: TextReplacerBuilder, yes: bool) -> TextReplacerBuilder {
 | 
			
		||||
    builder.regex(yes)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Sets whether the replacement should be case-insensitive
 | 
			
		||||
pub fn case_insensitive(builder: TextReplacerBuilder, yes: bool) -> TextReplacerBuilder {
 | 
			
		||||
    builder.case_insensitive(yes)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Adds another replacement operation to the chain and resets the builder for a new operation
 | 
			
		||||
pub fn and(builder: TextReplacerBuilder) -> TextReplacerBuilder {
 | 
			
		||||
    builder.and()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Builds the TextReplacer with all configured replacement operations
 | 
			
		||||
pub fn build(builder: TextReplacerBuilder) -> Result<TextReplacer, Box<EvalAltResult>> {
 | 
			
		||||
    string_error_to_rhai_error(builder.build())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Applies all configured replacement operations to the input text
 | 
			
		||||
pub fn replace(replacer: &mut TextReplacer, input: &str) -> String {
 | 
			
		||||
    replacer.replace(input)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Reads a file, applies all replacements, and returns the result as a string
 | 
			
		||||
pub fn replace_file(replacer: &mut TextReplacer, path: &str) -> Result<String, Box<EvalAltResult>> {
 | 
			
		||||
    io_error_to_rhai_error(replacer.replace_file(path))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Reads a file, applies all replacements, and writes the result back to the file
 | 
			
		||||
pub fn replace_file_in_place(
 | 
			
		||||
    replacer: &mut TextReplacer,
 | 
			
		||||
    path: &str,
 | 
			
		||||
) -> Result<(), Box<EvalAltResult>> {
 | 
			
		||||
    io_error_to_rhai_error(replacer.replace_file_in_place(path))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Reads a file, applies all replacements, and writes the result to a new file
 | 
			
		||||
pub fn replace_file_to(
 | 
			
		||||
    replacer: &mut TextReplacer,
 | 
			
		||||
    input_path: &str,
 | 
			
		||||
    output_path: &str,
 | 
			
		||||
) -> Result<(), Box<EvalAltResult>> {
 | 
			
		||||
    io_error_to_rhai_error(replacer.replace_file_to(input_path, output_path))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TemplateBuilder implementation
 | 
			
		||||
 | 
			
		||||
/// Creates a new TemplateBuilder with the specified template path
 | 
			
		||||
pub fn template_builder_open(template_path: &str) -> Result<TemplateBuilder, Box<EvalAltResult>> {
 | 
			
		||||
    io_error_to_rhai_error(TemplateBuilder::open(template_path))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Adds a string variable to the template context
 | 
			
		||||
pub fn add_var_string(builder: TemplateBuilder, name: &str, value: &str) -> TemplateBuilder {
 | 
			
		||||
    builder.add_var(name, value)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Adds an integer variable to the template context
 | 
			
		||||
pub fn add_var_int(builder: TemplateBuilder, name: &str, value: i64) -> TemplateBuilder {
 | 
			
		||||
    builder.add_var(name, value)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Adds a float variable to the template context
 | 
			
		||||
pub fn add_var_float(builder: TemplateBuilder, name: &str, value: f64) -> TemplateBuilder {
 | 
			
		||||
    builder.add_var(name, value)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Adds a boolean variable to the template context
 | 
			
		||||
pub fn add_var_bool(builder: TemplateBuilder, name: &str, value: bool) -> TemplateBuilder {
 | 
			
		||||
    builder.add_var(name, value)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Adds an array variable to the template context
 | 
			
		||||
pub fn add_var_array(builder: TemplateBuilder, name: &str, array: Array) -> TemplateBuilder {
 | 
			
		||||
    // Convert Rhai Array to Vec<String>
 | 
			
		||||
    let vec: Vec<String> = array
 | 
			
		||||
        .iter()
 | 
			
		||||
        .filter_map(|v| v.clone().into_string().ok())
 | 
			
		||||
        .collect();
 | 
			
		||||
 | 
			
		||||
    builder.add_var(name, vec)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Adds multiple variables to the template context from a Map
 | 
			
		||||
pub fn add_vars(builder: TemplateBuilder, vars: Map) -> TemplateBuilder {
 | 
			
		||||
    // Convert Rhai Map to Rust HashMap
 | 
			
		||||
    let mut hash_map = HashMap::new();
 | 
			
		||||
 | 
			
		||||
    for (key, value) in vars.iter() {
 | 
			
		||||
        if let Ok(val_str) = value.clone().into_string() {
 | 
			
		||||
            hash_map.insert(key.to_string(), val_str);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Add the variables
 | 
			
		||||
    builder.add_vars(hash_map)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Renders the template with the current context
 | 
			
		||||
pub fn render(builder: &mut TemplateBuilder) -> Result<String, Box<EvalAltResult>> {
 | 
			
		||||
    tera_error_to_rhai_error(builder.render())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Renders the template and writes the result to a file
 | 
			
		||||
pub fn render_to_file(
 | 
			
		||||
    builder: &mut TemplateBuilder,
 | 
			
		||||
    output_path: &str,
 | 
			
		||||
) -> Result<(), Box<EvalAltResult>> {
 | 
			
		||||
    io_error_to_rhai_error(builder.render_to_file(output_path))
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										310
									
								
								text/src/template.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										310
									
								
								text/src/template.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,310 @@
 | 
			
		||||
use std::collections::HashMap;
 | 
			
		||||
use std::fs;
 | 
			
		||||
use std::io;
 | 
			
		||||
use std::path::Path;
 | 
			
		||||
use tera::{Context, Tera};
 | 
			
		||||
 | 
			
		||||
/// A builder for creating and rendering templates using the Tera template engine.
 | 
			
		||||
#[derive(Clone)]
 | 
			
		||||
pub struct TemplateBuilder {
 | 
			
		||||
    template_path: String,
 | 
			
		||||
    context: Context,
 | 
			
		||||
    tera: Option<Tera>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl TemplateBuilder {
 | 
			
		||||
    /// Creates a new TemplateBuilder with the specified template path.
 | 
			
		||||
    ///
 | 
			
		||||
    /// # Arguments
 | 
			
		||||
    ///
 | 
			
		||||
    /// * `template_path` - The path to the template file
 | 
			
		||||
    ///
 | 
			
		||||
    /// # Returns
 | 
			
		||||
    ///
 | 
			
		||||
    /// A new TemplateBuilder instance
 | 
			
		||||
    ///
 | 
			
		||||
    /// # Example
 | 
			
		||||
    ///
 | 
			
		||||
    /// ```
 | 
			
		||||
    /// use sal::text::TemplateBuilder;
 | 
			
		||||
    ///
 | 
			
		||||
    /// let builder = TemplateBuilder::open("templates/example.html");
 | 
			
		||||
    /// ```
 | 
			
		||||
    pub fn open<P: AsRef<Path>>(template_path: P) -> io::Result<Self> {
 | 
			
		||||
        let path_str = template_path.as_ref().to_string_lossy().to_string();
 | 
			
		||||
 | 
			
		||||
        // Verify the template file exists
 | 
			
		||||
        if !Path::new(&path_str).exists() {
 | 
			
		||||
            return Err(io::Error::new(
 | 
			
		||||
                io::ErrorKind::NotFound,
 | 
			
		||||
                format!("Template file not found: {}", path_str),
 | 
			
		||||
            ));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(Self {
 | 
			
		||||
            template_path: path_str,
 | 
			
		||||
            context: Context::new(),
 | 
			
		||||
            tera: None,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Adds a variable to the template context.
 | 
			
		||||
    ///
 | 
			
		||||
    /// # Arguments
 | 
			
		||||
    ///
 | 
			
		||||
    /// * `name` - The name of the variable to add
 | 
			
		||||
    /// * `value` - The value to associate with the variable
 | 
			
		||||
    ///
 | 
			
		||||
    /// # Returns
 | 
			
		||||
    ///
 | 
			
		||||
    /// The builder instance for method chaining
 | 
			
		||||
    ///
 | 
			
		||||
    /// # Example
 | 
			
		||||
    ///
 | 
			
		||||
    /// ```no_run
 | 
			
		||||
    /// use sal::text::TemplateBuilder;
 | 
			
		||||
    ///
 | 
			
		||||
    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
 | 
			
		||||
    ///     let builder = TemplateBuilder::open("templates/example.html")?
 | 
			
		||||
    ///         .add_var("title", "Hello World")
 | 
			
		||||
    ///         .add_var("username", "John Doe");
 | 
			
		||||
    ///     Ok(())
 | 
			
		||||
    /// }
 | 
			
		||||
    /// ```
 | 
			
		||||
    pub fn add_var<S, V>(mut self, name: S, value: V) -> Self
 | 
			
		||||
    where
 | 
			
		||||
        S: AsRef<str>,
 | 
			
		||||
        V: serde::Serialize,
 | 
			
		||||
    {
 | 
			
		||||
        self.context.insert(name.as_ref(), &value);
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Adds multiple variables to the template context from a HashMap.
 | 
			
		||||
    ///
 | 
			
		||||
    /// # Arguments
 | 
			
		||||
    ///
 | 
			
		||||
    /// * `vars` - A HashMap containing variable names and values
 | 
			
		||||
    ///
 | 
			
		||||
    /// # Returns
 | 
			
		||||
    ///
 | 
			
		||||
    /// The builder instance for method chaining
 | 
			
		||||
    ///
 | 
			
		||||
    /// # Example
 | 
			
		||||
    ///
 | 
			
		||||
    /// ```no_run
 | 
			
		||||
    /// use sal::text::TemplateBuilder;
 | 
			
		||||
    /// use std::collections::HashMap;
 | 
			
		||||
    ///
 | 
			
		||||
    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
 | 
			
		||||
    ///     let mut vars = HashMap::new();
 | 
			
		||||
    ///     vars.insert("title", "Hello World");
 | 
			
		||||
    ///     vars.insert("username", "John Doe");
 | 
			
		||||
    ///
 | 
			
		||||
    ///     let builder = TemplateBuilder::open("templates/example.html")?
 | 
			
		||||
    ///         .add_vars(vars);
 | 
			
		||||
    ///     Ok(())
 | 
			
		||||
    /// }
 | 
			
		||||
    /// ```
 | 
			
		||||
    pub fn add_vars<S, V>(mut self, vars: HashMap<S, V>) -> Self
 | 
			
		||||
    where
 | 
			
		||||
        S: AsRef<str>,
 | 
			
		||||
        V: serde::Serialize,
 | 
			
		||||
    {
 | 
			
		||||
        for (name, value) in vars {
 | 
			
		||||
            self.context.insert(name.as_ref(), &value);
 | 
			
		||||
        }
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Initializes the Tera template engine with the template file.
 | 
			
		||||
    ///
 | 
			
		||||
    /// This method is called automatically by render() if not called explicitly.
 | 
			
		||||
    ///
 | 
			
		||||
    /// # Returns
 | 
			
		||||
    ///
 | 
			
		||||
    /// The builder instance for method chaining
 | 
			
		||||
    fn initialize_tera(&mut self) -> Result<(), tera::Error> {
 | 
			
		||||
        if self.tera.is_none() {
 | 
			
		||||
            // Create a new Tera instance with just this template
 | 
			
		||||
            let mut tera = Tera::default();
 | 
			
		||||
 | 
			
		||||
            // Read the template content
 | 
			
		||||
            let template_content = fs::read_to_string(&self.template_path)
 | 
			
		||||
                .map_err(|e| tera::Error::msg(format!("Failed to read template file: {}", e)))?;
 | 
			
		||||
 | 
			
		||||
            // Add the template to Tera
 | 
			
		||||
            let template_name = Path::new(&self.template_path)
 | 
			
		||||
                .file_name()
 | 
			
		||||
                .and_then(|n| n.to_str())
 | 
			
		||||
                .unwrap_or("template");
 | 
			
		||||
 | 
			
		||||
            tera.add_raw_template(template_name, &template_content)?;
 | 
			
		||||
            self.tera = Some(tera);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Renders the template with the current context.
 | 
			
		||||
    ///
 | 
			
		||||
    /// # Returns
 | 
			
		||||
    ///
 | 
			
		||||
    /// The rendered template as a string
 | 
			
		||||
    ///
 | 
			
		||||
    /// # Example
 | 
			
		||||
    ///
 | 
			
		||||
    /// ```no_run
 | 
			
		||||
    /// use sal::text::TemplateBuilder;
 | 
			
		||||
    ///
 | 
			
		||||
    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
 | 
			
		||||
    ///     let result = TemplateBuilder::open("templates/example.html")?
 | 
			
		||||
    ///         .add_var("title", "Hello World")
 | 
			
		||||
    ///         .add_var("username", "John Doe")
 | 
			
		||||
    ///         .render()?;
 | 
			
		||||
    ///
 | 
			
		||||
    ///     println!("Rendered template: {}", result);
 | 
			
		||||
    ///     Ok(())
 | 
			
		||||
    /// }
 | 
			
		||||
    /// ```
 | 
			
		||||
    pub fn render(&mut self) -> Result<String, tera::Error> {
 | 
			
		||||
        // Initialize Tera if not already done
 | 
			
		||||
        self.initialize_tera()?;
 | 
			
		||||
 | 
			
		||||
        // Get the template name
 | 
			
		||||
        let template_name = Path::new(&self.template_path)
 | 
			
		||||
            .file_name()
 | 
			
		||||
            .and_then(|n| n.to_str())
 | 
			
		||||
            .unwrap_or("template");
 | 
			
		||||
 | 
			
		||||
        // Render the template
 | 
			
		||||
        let tera = self.tera.as_ref().unwrap();
 | 
			
		||||
        tera.render(template_name, &self.context)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Renders the template and writes the result to a file.
 | 
			
		||||
    ///
 | 
			
		||||
    /// # Arguments
 | 
			
		||||
    ///
 | 
			
		||||
    /// * `output_path` - The path where the rendered template should be written
 | 
			
		||||
    ///
 | 
			
		||||
    /// # Returns
 | 
			
		||||
    ///
 | 
			
		||||
    /// Result indicating success or failure
 | 
			
		||||
    ///
 | 
			
		||||
    /// # Example
 | 
			
		||||
    ///
 | 
			
		||||
    /// ```no_run
 | 
			
		||||
    /// use sal::text::TemplateBuilder;
 | 
			
		||||
    ///
 | 
			
		||||
    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
 | 
			
		||||
    ///     TemplateBuilder::open("templates/example.html")?
 | 
			
		||||
    ///         .add_var("title", "Hello World")
 | 
			
		||||
    ///         .add_var("username", "John Doe")
 | 
			
		||||
    ///         .render_to_file("output.html")?;
 | 
			
		||||
    ///     Ok(())
 | 
			
		||||
    /// }
 | 
			
		||||
    /// ```
 | 
			
		||||
    pub fn render_to_file<P: AsRef<Path>>(&mut self, output_path: P) -> io::Result<()> {
 | 
			
		||||
        let rendered = self.render().map_err(|e| {
 | 
			
		||||
            io::Error::new(
 | 
			
		||||
                io::ErrorKind::Other,
 | 
			
		||||
                format!("Template rendering error: {}", e),
 | 
			
		||||
            )
 | 
			
		||||
        })?;
 | 
			
		||||
 | 
			
		||||
        fs::write(output_path, rendered)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod tests {
 | 
			
		||||
    use super::*;
 | 
			
		||||
    use std::io::Write;
 | 
			
		||||
    use tempfile::NamedTempFile;
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_template_rendering() -> Result<(), Box<dyn std::error::Error>> {
 | 
			
		||||
        // Create a temporary template file
 | 
			
		||||
        let temp_file = NamedTempFile::new()?;
 | 
			
		||||
        let template_content = "Hello, {{ name }}! Welcome to {{ place }}.\n";
 | 
			
		||||
        fs::write(temp_file.path(), template_content)?;
 | 
			
		||||
 | 
			
		||||
        // Create a template builder and add variables
 | 
			
		||||
        let mut builder = TemplateBuilder::open(temp_file.path())?;
 | 
			
		||||
        builder = builder.add_var("name", "John").add_var("place", "Rust");
 | 
			
		||||
 | 
			
		||||
        // Render the template
 | 
			
		||||
        let result = builder.render()?;
 | 
			
		||||
        assert_eq!(result, "Hello, John! Welcome to Rust.\n");
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_template_with_multiple_vars() -> Result<(), Box<dyn std::error::Error>> {
 | 
			
		||||
        // Create a temporary template file
 | 
			
		||||
        let temp_file = NamedTempFile::new()?;
 | 
			
		||||
        let template_content = "{% if show_greeting %}Hello, {{ name }}!{% endif %}\n{% for item in items %}{{ item }}{% if not loop.last %}, {% endif %}{% endfor %}\n";
 | 
			
		||||
        fs::write(temp_file.path(), template_content)?;
 | 
			
		||||
 | 
			
		||||
        // Create a template builder and add variables
 | 
			
		||||
        let mut builder = TemplateBuilder::open(temp_file.path())?;
 | 
			
		||||
 | 
			
		||||
        // Add variables including a boolean and a vector
 | 
			
		||||
        builder = builder
 | 
			
		||||
            .add_var("name", "Alice")
 | 
			
		||||
            .add_var("show_greeting", true)
 | 
			
		||||
            .add_var("items", vec!["apple", "banana", "cherry"]);
 | 
			
		||||
 | 
			
		||||
        // Render the template
 | 
			
		||||
        let result = builder.render()?;
 | 
			
		||||
        assert_eq!(result, "Hello, Alice!\napple, banana, cherry\n");
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_template_with_hashmap_vars() -> Result<(), Box<dyn std::error::Error>> {
 | 
			
		||||
        // Create a temporary template file
 | 
			
		||||
        let mut temp_file = NamedTempFile::new()?;
 | 
			
		||||
        writeln!(temp_file, "{{{{ greeting }}}}, {{{{ name }}}}!")?;
 | 
			
		||||
        temp_file.flush()?;
 | 
			
		||||
 | 
			
		||||
        // Create a HashMap of variables
 | 
			
		||||
        let mut vars = HashMap::new();
 | 
			
		||||
        vars.insert("greeting", "Hi");
 | 
			
		||||
        vars.insert("name", "Bob");
 | 
			
		||||
 | 
			
		||||
        // Create a template builder and add variables from HashMap
 | 
			
		||||
        let mut builder = TemplateBuilder::open(temp_file.path())?;
 | 
			
		||||
        builder = builder.add_vars(vars);
 | 
			
		||||
 | 
			
		||||
        // Render the template
 | 
			
		||||
        let result = builder.render()?;
 | 
			
		||||
        assert_eq!(result, "Hi, Bob!\n");
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_render_to_file() -> Result<(), Box<dyn std::error::Error>> {
 | 
			
		||||
        // Create a temporary template file
 | 
			
		||||
        let temp_file = NamedTempFile::new()?;
 | 
			
		||||
        let template_content = "{{ message }}\n";
 | 
			
		||||
        fs::write(temp_file.path(), template_content)?;
 | 
			
		||||
 | 
			
		||||
        // Create an output file
 | 
			
		||||
        let output_file = NamedTempFile::new()?;
 | 
			
		||||
 | 
			
		||||
        // Create a template builder, add a variable, and render to file
 | 
			
		||||
        let mut builder = TemplateBuilder::open(temp_file.path())?;
 | 
			
		||||
        builder = builder.add_var("message", "This is a test");
 | 
			
		||||
        builder.render_to_file(output_file.path())?;
 | 
			
		||||
 | 
			
		||||
        // Read the output file and verify its contents
 | 
			
		||||
        let content = fs::read_to_string(output_file.path())?;
 | 
			
		||||
        assert_eq!(content, "This is a test\n");
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user