feat: Add sal-text crate
Some checks failed
Rhai Tests / Run Rhai Tests (push) Has been cancelled

- 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:
Mahmoud-Emad
2025-06-19 14:43:27 +03:00
parent 4a8d3bfd24
commit a7a7353aa1
19 changed files with 1808 additions and 369 deletions

22
text/Cargo.toml Normal file
View File

@@ -0,0 +1,22 @@
[package]
name = "sal-text"
version = "0.1.0"
edition = "2021"
authors = ["PlanetFirst <info@incubaid.com>"]
description = "SAL Text - Text processing and manipulation utilities with regex, templating, and normalization"
repository = "https://git.threefold.info/herocode/sal"
license = "Apache-2.0"
[dependencies]
# Regex support for text replacement
regex = "1.8.1"
# Template engine for text rendering
tera = "1.19.0"
# Serialization support for templates
serde = { version = "1.0", features = ["derive"] }
# Rhai scripting support
rhai = { version = "1.12.0", features = ["sync"] }
[dev-dependencies]
# For temporary files in tests
tempfile = "3.5"

146
text/README.md Normal file
View File

@@ -0,0 +1,146 @@
# SAL Text - Text Processing and Manipulation Utilities
SAL Text provides a comprehensive collection of text processing utilities for both Rust applications and Rhai scripting environments.
## Features
- **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
## Rust API
### Text Indentation
```rust
use sal_text::{dedent, prefix};
// Remove common indentation
let indented = " line 1\n line 2\n line 3";
let dedented = dedent(indented);
assert_eq!(dedented, "line 1\nline 2\n line 3");
// Add prefix to each line
let text = "line 1\nline 2";
let prefixed = prefix(text, "> ");
assert_eq!(prefixed, "> line 1\n> line 2");
```
### String Normalization
```rust
use sal_text::{name_fix, path_fix};
// Sanitize filename
let unsafe_name = "User's File [Draft].txt";
let safe_name = name_fix(unsafe_name);
assert_eq!(safe_name, "user_s_file_draft_.txt");
// Sanitize path (preserves directory structure)
let unsafe_path = "/path/to/User's File.txt";
let safe_path = path_fix(unsafe_path);
assert_eq!(safe_path, "/path/to/user_s_file.txt");
```
### Text Replacement
```rust
use sal_text::TextReplacer;
// Simple literal replacement
let replacer = TextReplacer::builder()
.pattern("hello")
.replacement("hi")
.build()
.expect("Failed to build replacer");
let result = replacer.replace("hello world, hello universe");
assert_eq!(result, "hi world, hi universe");
// Regex replacement
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");
// Chained operations
let replacer = TextReplacer::builder()
.pattern("world")
.replacement("universe")
.and()
.pattern(r"\d+")
.replacement("NUMBER")
.regex(true)
.build()
.expect("Failed to build replacer");
```
### Template Rendering
```rust
use sal_text::TemplateBuilder;
let result = TemplateBuilder::open("template.txt")
.expect("Failed to open template")
.add_var("name", "World")
.add_var("count", 42)
.render()
.expect("Failed to render template");
```
## Rhai Scripting
All functionality is available in Rhai scripts when using `herodo`:
```rhai
// Text indentation
let dedented = dedent(" hello\n world");
let prefixed = prefix("line1\nline2", "> ");
// String normalization
let safe_name = name_fix("User's File [Draft].txt");
let safe_path = path_fix("/path/to/User's File.txt");
// Text replacement
let builder = text_replacer_new();
builder = pattern(builder, "hello");
builder = replacement(builder, "hi");
builder = regex(builder, false);
let replacer = build(builder);
let result = replace(replacer, "hello world");
// Template rendering
let template = template_builder_open("template.txt");
template = add_var(template, "name", "World");
let result = render(template);
```
## Testing
Run the comprehensive test suite:
```bash
# Unit tests
cargo test
# Rhai integration tests
cargo run --bin herodo tests/rhai/run_all_tests.rhai
```
## Dependencies
- `regex`: For regex-based text replacement
- `tera`: For template rendering
- `serde`: For template variable serialization
- `rhai`: For Rhai scripting integration
## License
Apache-2.0

137
text/src/dedent.rs Normal file
View 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
View 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
View 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
View 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(&regex_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
View 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
View 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(())
}
}

View File

@@ -0,0 +1,255 @@
// Text Rhai Test Runner
//
// This script runs all Text-related Rhai tests and reports results.
print("=== Text Rhai Test Suite ===");
print("Running comprehensive tests for Text Rhai integration...\n");
let total_tests = 0;
let passed_tests = 0;
let failed_tests = 0;
// Test 1: Text Indentation Functions
print("Test 1: Text Indentation Functions");
total_tests += 1;
try {
let indented = " line 1\n line 2\n line 3";
let dedented = dedent(indented);
let text = "line 1\nline 2";
let prefixed = prefix(text, "> ");
if dedented == "line 1\nline 2\n line 3" && prefixed == "> line 1\n> line 2" {
passed_tests += 1;
print("✓ PASSED: Text indentation functions work correctly");
} else {
failed_tests += 1;
print("✗ FAILED: Text indentation functions returned unexpected results");
}
} catch(err) {
failed_tests += 1;
print(`✗ ERROR: Text indentation test failed - ${err}`);
}
// Test 2: String Normalization Functions
print("\nTest 2: String Normalization Functions");
total_tests += 1;
try {
let unsafe_name = "User's File [Draft].txt";
let safe_name = name_fix(unsafe_name);
let unsafe_path = "/path/to/User's File.txt";
let safe_path = path_fix(unsafe_path);
if safe_name == "user_s_file_draft_.txt" && safe_path == "/path/to/user_s_file.txt" {
passed_tests += 1;
print("✓ PASSED: String normalization functions work correctly");
} else {
failed_tests += 1;
print(`✗ FAILED: String normalization - expected 'user_s_file_draft_.txt' and '/path/to/user_s_file.txt', got '${safe_name}' and '${safe_path}'`);
}
} catch(err) {
failed_tests += 1;
print(`✗ ERROR: String normalization test failed - ${err}`);
}
// Test 3: TextReplacer Builder Pattern
print("\nTest 3: TextReplacer Builder Pattern");
total_tests += 1;
try {
let builder = text_replacer_new();
builder = pattern(builder, "hello");
builder = replacement(builder, "hi");
builder = regex(builder, false);
let replacer = build(builder);
let result = replace(replacer, "hello world, hello universe");
if result == "hi world, hi universe" {
passed_tests += 1;
print("✓ PASSED: TextReplacer builder pattern works correctly");
} else {
failed_tests += 1;
print(`✗ FAILED: TextReplacer - expected 'hi world, hi universe', got '${result}'`);
}
} catch(err) {
failed_tests += 1;
print(`✗ ERROR: TextReplacer builder test failed - ${err}`);
}
// Test 4: TextReplacer with Regex
print("\nTest 4: TextReplacer with Regex");
total_tests += 1;
try {
let builder = text_replacer_new();
builder = pattern(builder, "\\d+");
builder = replacement(builder, "NUMBER");
builder = regex(builder, true);
let replacer = build(builder);
let result = replace(replacer, "There are 123 items and 456 more");
if result == "There are NUMBER items and NUMBER more" {
passed_tests += 1;
print("✓ PASSED: TextReplacer regex functionality works correctly");
} else {
failed_tests += 1;
print(`✗ FAILED: TextReplacer regex - expected 'There are NUMBER items and NUMBER more', got '${result}'`);
}
} catch(err) {
failed_tests += 1;
print(`✗ ERROR: TextReplacer regex test failed - ${err}`);
}
// Test 5: TextReplacer Chained Operations
print("\nTest 5: TextReplacer Chained Operations");
total_tests += 1;
try {
let builder = text_replacer_new();
builder = pattern(builder, "world");
builder = replacement(builder, "universe");
builder = regex(builder, false);
builder = and(builder);
builder = pattern(builder, "\\d+");
builder = replacement(builder, "NUMBER");
builder = regex(builder, true);
let replacer = build(builder);
let result = replace(replacer, "Hello world, there are 123 items");
if result == "Hello universe, there are NUMBER items" {
passed_tests += 1;
print("✓ PASSED: TextReplacer chained operations work correctly");
} else {
failed_tests += 1;
print(`✗ FAILED: TextReplacer chained - expected 'Hello universe, there are NUMBER items', got '${result}'`);
}
} catch(err) {
failed_tests += 1;
print(`✗ ERROR: TextReplacer chained operations test failed - ${err}`);
}
// Test 6: Error Handling - Invalid Regex
print("\nTest 6: Error Handling - Invalid Regex");
total_tests += 1;
try {
let builder = text_replacer_new();
builder = pattern(builder, "[invalid regex");
builder = replacement(builder, "test");
builder = regex(builder, true);
let replacer = build(builder);
failed_tests += 1;
print("✗ FAILED: Should have failed with invalid regex");
} catch(err) {
passed_tests += 1;
print("✓ PASSED: Invalid regex properly rejected");
}
// Test 7: Unicode Handling
print("\nTest 7: Unicode Handling");
total_tests += 1;
try {
let unicode_text = " Hello 世界\n Goodbye 世界";
let dedented = dedent(unicode_text);
let unicode_name = "Café";
let fixed_name = name_fix(unicode_name);
let unicode_prefix = prefix("Hello 世界", "🔹 ");
if dedented == "Hello 世界\nGoodbye 世界" &&
fixed_name == "caf" &&
unicode_prefix == "🔹 Hello 世界" {
passed_tests += 1;
print("✓ PASSED: Unicode handling works correctly");
} else {
failed_tests += 1;
print("✗ FAILED: Unicode handling returned unexpected results");
}
} catch(err) {
failed_tests += 1;
print(`✗ ERROR: Unicode handling test failed - ${err}`);
}
// Test 8: Edge Cases
print("\nTest 8: Edge Cases");
total_tests += 1;
try {
let empty_dedent = dedent("");
let empty_prefix = prefix("test", "");
let empty_name_fix = name_fix("");
if empty_dedent == "" && empty_prefix == "test" && empty_name_fix == "" {
passed_tests += 1;
print("✓ PASSED: Edge cases handled correctly");
} else {
failed_tests += 1;
print("✗ FAILED: Edge cases returned unexpected results");
}
} catch(err) {
failed_tests += 1;
print(`✗ ERROR: Edge cases test failed - ${err}`);
}
// Test 9: Complex Workflow
print("\nTest 9: Complex Text Processing Workflow");
total_tests += 1;
try {
// Normalize filename
let unsafe_filename = "User's Script [Draft].py";
let safe_filename = name_fix(unsafe_filename);
// Process code
let indented_code = " def hello():\n print('Hello World')\n return True";
let dedented_code = dedent(indented_code);
let commented_code = prefix(dedented_code, "# ");
// Replace text
let builder = text_replacer_new();
builder = pattern(builder, "Hello World");
builder = replacement(builder, "SAL Text");
builder = regex(builder, false);
let replacer = build(builder);
let final_code = replace(replacer, commented_code);
if safe_filename == "user_s_script_draft_.py" &&
final_code.contains("# def hello():") &&
final_code.contains("SAL Text") {
passed_tests += 1;
print("✓ PASSED: Complex workflow completed successfully");
} else {
failed_tests += 1;
print("✗ FAILED: Complex workflow returned unexpected results");
}
} catch(err) {
failed_tests += 1;
print(`✗ ERROR: Complex workflow test failed - ${err}`);
}
// Test 10: Template Builder Error Handling
print("\nTest 10: Template Builder Error Handling");
total_tests += 1;
try {
let builder = template_builder_open("/nonexistent/file.txt");
failed_tests += 1;
print("✗ FAILED: Should have failed with nonexistent file");
} catch(err) {
passed_tests += 1;
print("✓ PASSED: Template builder properly handles nonexistent files");
}
// Print final results
print("\n=== Test Results ===");
print(`Total Tests: ${total_tests}`);
print(`Passed: ${passed_tests}`);
print(`Failed: ${failed_tests}`);
if failed_tests == 0 {
print("\n✓ All tests passed!");
} else {
print(`\n✗ ${failed_tests} test(s) failed.`);
}
print("\n=== Text Rhai Test Suite Completed ===");

View File

@@ -0,0 +1,351 @@
//! Rhai integration tests for Text module
//!
//! These tests validate the Rhai wrapper functions and ensure proper
//! integration between Rust and Rhai for text processing operations.
use rhai::{Engine, EvalAltResult};
use sal_text::rhai::*;
#[cfg(test)]
mod rhai_integration_tests {
use super::*;
fn create_test_engine() -> Engine {
let mut engine = Engine::new();
register_text_module(&mut engine).expect("Failed to register text module");
engine
}
#[test]
fn test_rhai_module_registration() {
let engine = create_test_engine();
// Test that the functions are registered by checking if they exist
let script = r#"
// Test that all text functions are available
let functions_exist = true;
functions_exist
"#;
let result: Result<bool, Box<EvalAltResult>> = engine.eval(script);
assert!(result.is_ok());
assert_eq!(result.unwrap(), true);
}
#[test]
fn test_dedent_function_exists() {
let engine = create_test_engine();
let script = r#"
let indented = " line 1\n line 2\n line 3";
let result = dedent(indented);
return result == "line 1\nline 2\n line 3";
"#;
let result: Result<bool, Box<EvalAltResult>> = engine.eval(script);
assert!(result.is_ok());
assert_eq!(result.unwrap(), true);
}
#[test]
fn test_prefix_function_exists() {
let engine = create_test_engine();
let script = r#"
let text = "line 1\nline 2";
let result = prefix(text, "> ");
return result == "> line 1\n> line 2";
"#;
let result: Result<bool, Box<EvalAltResult>> = engine.eval(script);
assert!(result.is_ok());
assert_eq!(result.unwrap(), true);
}
#[test]
fn test_name_fix_function_exists() {
let engine = create_test_engine();
let script = r#"
let unsafe_name = "User's File [Draft].txt";
let result = name_fix(unsafe_name);
return result == "users_file_draft_.txt";
"#;
let result: Result<bool, Box<EvalAltResult>> = engine.eval(script);
assert!(result.is_ok());
assert_eq!(result.unwrap(), true);
}
#[test]
fn test_path_fix_function_exists() {
let engine = create_test_engine();
let script = r#"
let unsafe_path = "/path/to/User's File.txt";
let result = path_fix(unsafe_path);
return result == "/path/to/users_file.txt";
"#;
let result: Result<bool, Box<EvalAltResult>> = engine.eval(script);
assert!(result.is_ok());
assert_eq!(result.unwrap(), true);
}
#[test]
fn test_text_replacer_builder_creation() {
let engine = create_test_engine();
let script = r#"
let builder = text_replacer_builder();
return type_of(builder) == "sal_text::replace::TextReplacerBuilder";
"#;
let result: Result<bool, Box<EvalAltResult>> = engine.eval(script);
assert!(result.is_ok());
assert_eq!(result.unwrap(), true);
}
#[test]
fn test_text_replacer_workflow() {
let engine = create_test_engine();
let script = r#"
let builder = text_replacer_builder();
builder = pattern(builder, "hello");
builder = replacement(builder, "hi");
builder = regex(builder, false);
let replacer = build(builder);
let result = replace(replacer, "hello world, hello universe");
return result == "hi world, hi universe";
"#;
let result: Result<bool, Box<EvalAltResult>> = engine.eval(script);
assert!(result.is_ok());
assert_eq!(result.unwrap(), true);
}
#[test]
fn test_text_replacer_regex_workflow() {
let engine = create_test_engine();
let script = r#"
let builder = text_replacer_builder();
builder = pattern(builder, r"\d+");
builder = replacement(builder, "NUMBER");
builder = regex(builder, true);
let replacer = build(builder);
let result = replace(replacer, "There are 123 items");
return result == "There are NUMBER items";
"#;
let result: Result<bool, Box<EvalAltResult>> = engine.eval(script);
assert!(result.is_ok());
assert_eq!(result.unwrap(), true);
}
#[test]
fn test_text_replacer_chained_operations() {
let engine = create_test_engine();
let script = r#"
let builder = text_replacer_builder();
builder = pattern(builder, "world");
builder = replacement(builder, "universe");
builder = regex(builder, false);
builder = and(builder);
builder = pattern(builder, r"\d+");
builder = replacement(builder, "NUMBER");
builder = regex(builder, true);
let replacer = build(builder);
let result = replace(replacer, "Hello world, there are 123 items");
return result == "Hello universe, there are NUMBER items";
"#;
let result: Result<bool, Box<EvalAltResult>> = engine.eval(script);
assert!(result.is_ok());
assert_eq!(result.unwrap(), true);
}
#[test]
fn test_template_builder_creation() {
let engine = create_test_engine();
let script = r#"
// We can't test file operations easily in unit tests,
// but we can test that the function exists and returns the right type
try {
let builder = template_builder_open("/nonexistent/file.txt");
return false; // Should have failed
} catch(err) {
return err.to_string().contains("error"); // Expected to fail
}
"#;
let result: Result<bool, Box<EvalAltResult>> = engine.eval(script);
assert!(result.is_ok());
assert_eq!(result.unwrap(), true);
}
#[test]
fn test_error_handling_invalid_regex() {
let engine = create_test_engine();
let script = r#"
try {
let builder = text_replacer_builder();
builder = pattern(builder, "[invalid regex");
builder = replacement(builder, "test");
builder = regex(builder, true);
let replacer = build(builder);
return false; // Should have failed
} catch(err) {
return true; // Expected to fail
}
"#;
let result: Result<bool, Box<EvalAltResult>> = engine.eval(script);
assert!(result.is_ok());
assert_eq!(result.unwrap(), true);
}
#[test]
fn test_parameter_validation() {
let engine = create_test_engine();
// Test that functions handle parameter validation correctly
let script = r#"
let test_results = [];
// Test empty string handling
try {
let result = dedent("");
test_results.push(result == "");
} catch(err) {
test_results.push(false);
}
// Test empty prefix
try {
let result = prefix("test", "");
test_results.push(result == "test");
} catch(err) {
test_results.push(false);
}
// Test empty name_fix
try {
let result = name_fix("");
test_results.push(result == "");
} catch(err) {
test_results.push(false);
}
return test_results;
"#;
let result: Result<rhai::Array, Box<EvalAltResult>> = engine.eval(script);
assert!(result.is_ok());
let results = result.unwrap();
// All parameter validation tests should pass
for (i, result) in results.iter().enumerate() {
assert_eq!(
result.as_bool().unwrap_or(false),
true,
"Parameter validation test {} failed",
i
);
}
}
#[test]
fn test_unicode_handling() {
let engine = create_test_engine();
let script = r#"
let unicode_tests = [];
// Test dedent with unicode
try {
let text = " Hello 世界\n Goodbye 世界";
let result = dedent(text);
unicode_tests.push(result == "Hello 世界\nGoodbye 世界");
} catch(err) {
unicode_tests.push(false);
}
// Test name_fix with unicode (should remove non-ASCII)
try {
let result = name_fix("Café");
unicode_tests.push(result == "caf");
} catch(err) {
unicode_tests.push(false);
}
// Test prefix with unicode
try {
let result = prefix("Hello 世界", "🔹 ");
unicode_tests.push(result == "🔹 Hello 世界");
} catch(err) {
unicode_tests.push(false);
}
return unicode_tests;
"#;
let result: Result<rhai::Array, Box<EvalAltResult>> = engine.eval(script);
assert!(result.is_ok());
let results = result.unwrap();
// All unicode tests should pass
for (i, result) in results.iter().enumerate() {
assert_eq!(
result.as_bool().unwrap_or(false),
true,
"Unicode test {} failed",
i
);
}
}
#[test]
fn test_complex_text_processing_workflow() {
let engine = create_test_engine();
let script = r#"
// Simple workflow test
let unsafe_filename = "User's Script [Draft].py";
let safe_filename = name_fix(unsafe_filename);
let indented_code = " def hello():\n return True";
let dedented_code = dedent(indented_code);
let results = [];
results.push(safe_filename == "users_script_draft_.py");
results.push(dedented_code.contains("def hello():"));
return results;
"#;
let result: Result<rhai::Array, Box<EvalAltResult>> = engine.eval(script);
assert!(result.is_ok());
let results = result.unwrap();
// All workflow tests should pass
for (i, result) in results.iter().enumerate() {
assert_eq!(
result.as_bool().unwrap_or(false),
true,
"Workflow test {} failed",
i
);
}
}
}

View File

@@ -0,0 +1,174 @@
//! Unit tests for string normalization functionality
//!
//! These tests validate the name_fix and path_fix functions including:
//! - Filename sanitization for safe filesystem usage
//! - Path normalization preserving directory structure
//! - Special character handling and replacement
//! - Unicode character removal and ASCII conversion
use sal_text::{name_fix, path_fix};
#[test]
fn test_name_fix_basic() {
assert_eq!(name_fix("Hello World"), "hello_world");
assert_eq!(name_fix("File-Name.txt"), "file_name.txt");
}
#[test]
fn test_name_fix_special_characters() {
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]
fn test_name_fix_unicode_removal() {
assert_eq!(name_fix("Café"), "caf");
assert_eq!(name_fix("Résumé"), "rsum");
assert_eq!(name_fix("Über"), "ber");
assert_eq!(name_fix("Naïve"), "nave");
assert_eq!(name_fix("Piñata"), "piata");
}
#[test]
fn test_name_fix_case_conversion() {
assert_eq!(name_fix("UPPERCASE"), "uppercase");
assert_eq!(name_fix("MixedCase"), "mixedcase");
assert_eq!(name_fix("camelCase"), "camelcase");
assert_eq!(name_fix("PascalCase"), "pascalcase");
}
#[test]
fn test_name_fix_consecutive_underscores() {
assert_eq!(name_fix("Multiple Spaces"), "multiple_spaces");
assert_eq!(name_fix("Special!!!Characters"), "special_characters");
assert_eq!(name_fix("Mixed-_-Separators"), "mixed_separators");
}
#[test]
fn test_name_fix_file_extensions() {
assert_eq!(name_fix("Document.PDF"), "document.pdf");
assert_eq!(name_fix("Image.JPEG"), "image.jpeg");
assert_eq!(name_fix("Archive.tar.gz"), "archive.tar.gz");
assert_eq!(name_fix("Config.json"), "config.json");
}
#[test]
fn test_name_fix_empty_and_edge_cases() {
assert_eq!(name_fix(""), "");
assert_eq!(name_fix(" "), "_");
assert_eq!(name_fix("!!!"), "_");
assert_eq!(name_fix("___"), "_");
}
#[test]
fn test_name_fix_real_world_examples() {
assert_eq!(name_fix("User's Report [Draft 1].md"), "users_report_draft_1_.md");
assert_eq!(name_fix("Meeting Notes (2023-12-01).txt"), "meeting_notes_2023_12_01_.txt");
assert_eq!(name_fix("Photo #123 - Vacation!.jpg"), "photo_123_vacation_.jpg");
assert_eq!(name_fix("Project Plan v2.0 FINAL.docx"), "project_plan_v2.0_final.docx");
}
#[test]
fn test_path_fix_directory_paths() {
assert_eq!(path_fix("/path/to/directory/"), "/path/to/directory/");
assert_eq!(path_fix("./relative/path/"), "./relative/path/");
assert_eq!(path_fix("../parent/path/"), "../parent/path/");
}
#[test]
fn test_path_fix_single_filename() {
assert_eq!(path_fix("filename.txt"), "filename.txt");
assert_eq!(path_fix("UPPER-file.md"), "upper_file.md");
assert_eq!(path_fix("Special!File.pdf"), "special_file.pdf");
}
#[test]
fn test_path_fix_absolute_paths() {
assert_eq!(path_fix("/path/to/File Name.txt"), "/path/to/file_name.txt");
assert_eq!(path_fix("/absolute/path/to/DOCUMENT-123.pdf"), "/absolute/path/to/document_123.pdf");
assert_eq!(path_fix("/home/user/Résumé.doc"), "/home/user/rsum.doc");
}
#[test]
fn test_path_fix_relative_paths() {
assert_eq!(path_fix("./relative/path/to/Document.PDF"), "./relative/path/to/document.pdf");
assert_eq!(path_fix("../parent/Special File.txt"), "../parent/special_file.txt");
assert_eq!(path_fix("subfolder/User's File.md"), "subfolder/users_file.md");
}
#[test]
fn test_path_fix_special_characters_in_filename() {
assert_eq!(path_fix("/path/with/[special]<chars>.txt"), "/path/with/_special_chars_.txt");
assert_eq!(path_fix("./folder/File!@#.pdf"), "./folder/file_.pdf");
assert_eq!(path_fix("/data/Report (Final).docx"), "/data/report_final_.docx");
}
#[test]
fn test_path_fix_preserves_path_structure() {
assert_eq!(path_fix("/very/long/path/to/some/Deep File.txt"), "/very/long/path/to/some/deep_file.txt");
assert_eq!(path_fix("./a/b/c/d/e/Final Document.pdf"), "./a/b/c/d/e/final_document.pdf");
}
#[test]
fn test_path_fix_windows_style_paths() {
// Note: These tests assume Unix-style path handling
// In a real implementation, you might want to handle Windows paths differently
assert_eq!(path_fix("C:\\Users\\Name\\Document.txt"), "c_users_name_document.txt");
}
#[test]
fn test_path_fix_edge_cases() {
assert_eq!(path_fix(""), "");
assert_eq!(path_fix("/"), "/");
assert_eq!(path_fix("./"), "./");
assert_eq!(path_fix("../"), "../");
}
#[test]
fn test_path_fix_unicode_in_filename() {
assert_eq!(path_fix("/path/to/Café.txt"), "/path/to/caf.txt");
assert_eq!(path_fix("./folder/Naïve Document.pdf"), "./folder/nave_document.pdf");
assert_eq!(path_fix("/home/user/Piñata Party.jpg"), "/home/user/piata_party.jpg");
}
#[test]
fn test_path_fix_complex_real_world_examples() {
assert_eq!(
path_fix("/Users/john/Documents/Project Files/Final Report (v2.1) [APPROVED].docx"),
"/Users/john/Documents/Project Files/final_report_v2.1_approved_.docx"
);
assert_eq!(
path_fix("./assets/images/Photo #123 - Vacation! (2023).jpg"),
"./assets/images/photo_123_vacation_2023_.jpg"
);
assert_eq!(
path_fix("/var/log/Application Logs/Error Log [2023-12-01].txt"),
"/var/log/Application Logs/error_log_2023_12_01_.txt"
);
}
#[test]
fn test_name_fix_and_path_fix_consistency() {
let filename = "User's Report [Draft].txt";
let path = "/path/to/User's Report [Draft].txt";
let fixed_name = name_fix(filename);
let fixed_path = path_fix(path);
// The filename part should be the same in both cases
assert!(fixed_path.ends_with(&fixed_name));
assert_eq!(fixed_name, "users_report_draft_.txt");
assert_eq!(fixed_path, "/path/to/users_report_draft_.txt");
}
#[test]
fn test_normalization_preserves_dots_in_extensions() {
assert_eq!(name_fix("file.tar.gz"), "file.tar.gz");
assert_eq!(name_fix("backup.2023.12.01.sql"), "backup.2023.12.01.sql");
assert_eq!(path_fix("/path/to/archive.tar.bz2"), "/path/to/archive.tar.bz2");
}

View File

@@ -0,0 +1,297 @@
//! Unit tests for template functionality
//!
//! These tests validate the TemplateBuilder including:
//! - Template loading from files
//! - Variable substitution (string, int, float, bool, array)
//! - Template rendering to string and file
//! - Error handling for missing variables and invalid templates
//! - Complex template scenarios with loops and conditionals
use sal_text::TemplateBuilder;
use std::collections::HashMap;
use std::fs;
use tempfile::NamedTempFile;
#[test]
fn test_template_builder_basic_string_variable() {
// Create a temporary template file
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
let template_content = "Hello {{name}}!";
fs::write(temp_file.path(), template_content).expect("Failed to write template");
let result = TemplateBuilder::open(temp_file.path())
.expect("Failed to open template")
.add_var("name", "World")
.render()
.expect("Failed to render template");
assert_eq!(result, "Hello World!");
}
#[test]
fn test_template_builder_multiple_variables() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
let template_content = "{{greeting}} {{name}}, you have {{count}} messages.";
fs::write(temp_file.path(), template_content).expect("Failed to write template");
let result = TemplateBuilder::open(temp_file.path())
.expect("Failed to open template")
.add_var("greeting", "Hello")
.add_var("name", "Alice")
.add_var("count", 5)
.render()
.expect("Failed to render template");
assert_eq!(result, "Hello Alice, you have 5 messages.");
}
#[test]
fn test_template_builder_different_types() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
let template_content = "String: {{text}}, Int: {{number}}, Float: {{decimal}}, Bool: {{flag}}";
fs::write(temp_file.path(), template_content).expect("Failed to write template");
let result = TemplateBuilder::open(temp_file.path())
.expect("Failed to open template")
.add_var("text", "hello")
.add_var("number", 42)
.add_var("decimal", 3.14)
.add_var("flag", true)
.render()
.expect("Failed to render template");
assert_eq!(result, "String: hello, Int: 42, Float: 3.14, Bool: true");
}
#[test]
fn test_template_builder_array_variable() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
let template_content = "Items: {% for item in items %}{{item}}{% if not loop.last %}, {% endif %}{% endfor %}";
fs::write(temp_file.path(), template_content).expect("Failed to write template");
let items = vec!["apple", "banana", "cherry"];
let result = TemplateBuilder::open(temp_file.path())
.expect("Failed to open template")
.add_var("items", items)
.render()
.expect("Failed to render template");
assert_eq!(result, "Items: apple, banana, cherry");
}
#[test]
fn test_template_builder_add_vars_hashmap() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
let template_content = "{{title}}: {{description}}";
fs::write(temp_file.path(), template_content).expect("Failed to write template");
let mut vars = HashMap::new();
vars.insert("title".to_string(), "Report".to_string());
vars.insert("description".to_string(), "Monthly summary".to_string());
let result = TemplateBuilder::open(temp_file.path())
.expect("Failed to open template")
.add_vars(vars)
.render()
.expect("Failed to render template");
assert_eq!(result, "Report: Monthly summary");
}
#[test]
fn test_template_builder_render_to_file() {
// Create template file
let mut template_file = NamedTempFile::new().expect("Failed to create template file");
let template_content = "Hello {{name}}, today is {{day}}.";
fs::write(template_file.path(), template_content).expect("Failed to write template");
// Create output file
let output_file = NamedTempFile::new().expect("Failed to create output file");
TemplateBuilder::open(template_file.path())
.expect("Failed to open template")
.add_var("name", "Bob")
.add_var("day", "Monday")
.render_to_file(output_file.path())
.expect("Failed to render to file");
let result = fs::read_to_string(output_file.path()).expect("Failed to read output file");
assert_eq!(result, "Hello Bob, today is Monday.");
}
#[test]
fn test_template_builder_conditional() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
let template_content = "{% if show_message %}Message: {{message}}{% else %}No message{% endif %}";
fs::write(temp_file.path(), template_content).expect("Failed to write template");
// Test with condition true
let result_true = TemplateBuilder::open(temp_file.path())
.expect("Failed to open template")
.add_var("show_message", true)
.add_var("message", "Hello World")
.render()
.expect("Failed to render template");
assert_eq!(result_true, "Message: Hello World");
// Test with condition false
let result_false = TemplateBuilder::open(temp_file.path())
.expect("Failed to open template")
.add_var("show_message", false)
.add_var("message", "Hello World")
.render()
.expect("Failed to render template");
assert_eq!(result_false, "No message");
}
#[test]
fn test_template_builder_loop_with_index() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
let template_content = "{% for item in items %}{{loop.index}}: {{item}}\n{% endfor %}";
fs::write(temp_file.path(), template_content).expect("Failed to write template");
let items = vec!["first", "second", "third"];
let result = TemplateBuilder::open(temp_file.path())
.expect("Failed to open template")
.add_var("items", items)
.render()
.expect("Failed to render template");
assert_eq!(result, "1: first\n2: second\n3: third\n");
}
#[test]
fn test_template_builder_nested_variables() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
let template_content = "User: {{user.name}} ({{user.email}})";
fs::write(temp_file.path(), template_content).expect("Failed to write template");
let mut user = HashMap::new();
user.insert("name".to_string(), "John Doe".to_string());
user.insert("email".to_string(), "john@example.com".to_string());
let result = TemplateBuilder::open(temp_file.path())
.expect("Failed to open template")
.add_var("user", user)
.render()
.expect("Failed to render template");
assert_eq!(result, "User: John Doe (john@example.com)");
}
#[test]
fn test_template_builder_missing_variable_error() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
let template_content = "Hello {{missing_var}}!";
fs::write(temp_file.path(), template_content).expect("Failed to write template");
let result = TemplateBuilder::open(temp_file.path())
.expect("Failed to open template")
.render();
assert!(result.is_err());
}
#[test]
fn test_template_builder_invalid_template_syntax() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
let template_content = "Hello {{unclosed_var!";
fs::write(temp_file.path(), template_content).expect("Failed to write template");
let result = TemplateBuilder::open(temp_file.path())
.expect("Failed to open template")
.render();
assert!(result.is_err());
}
#[test]
fn test_template_builder_nonexistent_file() {
let result = TemplateBuilder::open("/nonexistent/template.txt");
assert!(result.is_err());
}
#[test]
fn test_template_builder_empty_template() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
fs::write(temp_file.path(), "").expect("Failed to write empty template");
let result = TemplateBuilder::open(temp_file.path())
.expect("Failed to open template")
.render()
.expect("Failed to render empty template");
assert_eq!(result, "");
}
#[test]
fn test_template_builder_template_with_no_variables() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
let template_content = "This is a static template with no variables.";
fs::write(temp_file.path(), template_content).expect("Failed to write template");
let result = TemplateBuilder::open(temp_file.path())
.expect("Failed to open template")
.render()
.expect("Failed to render template");
assert_eq!(result, template_content);
}
#[test]
fn test_template_builder_complex_report() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
let template_content = r#"
# {{report_title}}
Generated on: {{date}}
## Summary
Total items: {{total_items}}
Status: {{status}}
## Items
{% for item in items %}
- {{item.name}}: {{item.value}}{% if item.important %} (IMPORTANT){% endif %}
{% endfor %}
## Footer
{% if show_footer %}
Report generated by {{generator}}
{% endif %}
"#;
fs::write(temp_file.path(), template_content).expect("Failed to write template");
let mut item1 = HashMap::new();
item1.insert("name".to_string(), "Item 1".to_string());
item1.insert("value".to_string(), "100".to_string());
item1.insert("important".to_string(), true.to_string());
let mut item2 = HashMap::new();
item2.insert("name".to_string(), "Item 2".to_string());
item2.insert("value".to_string(), "200".to_string());
item2.insert("important".to_string(), false.to_string());
let items = vec![item1, item2];
let result = TemplateBuilder::open(temp_file.path())
.expect("Failed to open template")
.add_var("report_title", "Monthly Report")
.add_var("date", "2023-12-01")
.add_var("total_items", 2)
.add_var("status", "Complete")
.add_var("items", items)
.add_var("show_footer", true)
.add_var("generator", "SAL Text")
.render()
.expect("Failed to render template");
assert!(result.contains("# Monthly Report"));
assert!(result.contains("Generated on: 2023-12-01"));
assert!(result.contains("Total items: 2"));
assert!(result.contains("- Item 1: 100"));
assert!(result.contains("- Item 2: 200"));
assert!(result.contains("Report generated by SAL Text"));
}

View File

@@ -0,0 +1,159 @@
//! Unit tests for text indentation functionality
//!
//! These tests validate the dedent and prefix functions including:
//! - Common whitespace removal (dedent)
//! - Line prefix addition (prefix)
//! - Edge cases and special characters
//! - Tab handling and mixed indentation
use sal_text::{dedent, prefix};
#[test]
fn test_dedent_basic() {
let indented = " line 1\n line 2\n line 3";
let expected = "line 1\nline 2\n line 3";
assert_eq!(dedent(indented), expected);
}
#[test]
fn test_dedent_empty_lines() {
let indented = " line 1\n\n line 2\n line 3";
let expected = "line 1\n\nline 2\n line 3";
assert_eq!(dedent(indented), expected);
}
#[test]
fn test_dedent_tabs_as_spaces() {
let indented = "\t\tline 1\n\t\tline 2\n\t\t\tline 3";
let expected = "line 1\nline 2\n\tline 3";
assert_eq!(dedent(indented), expected);
}
#[test]
fn test_dedent_mixed_tabs_and_spaces() {
let indented = " \tline 1\n \tline 2\n \t line 3";
let expected = "line 1\nline 2\n line 3";
assert_eq!(dedent(indented), expected);
}
#[test]
fn test_dedent_no_common_indentation() {
let text = "line 1\n line 2\n line 3";
let expected = "line 1\n line 2\n line 3";
assert_eq!(dedent(text), expected);
}
#[test]
fn test_dedent_single_line() {
let indented = " single line";
let expected = "single line";
assert_eq!(dedent(indented), expected);
}
#[test]
fn test_dedent_empty_string() {
assert_eq!(dedent(""), "");
}
#[test]
fn test_dedent_only_whitespace() {
let whitespace = " \n \n ";
let expected = "\n\n";
assert_eq!(dedent(whitespace), expected);
}
#[test]
fn test_prefix_basic() {
let text = "line 1\nline 2\nline 3";
let expected = " line 1\n line 2\n line 3";
assert_eq!(prefix(text, " "), expected);
}
#[test]
fn test_prefix_with_symbols() {
let text = "line 1\nline 2\nline 3";
let expected = "> line 1\n> line 2\n> line 3";
assert_eq!(prefix(text, "> "), expected);
}
#[test]
fn test_prefix_empty_lines() {
let text = "line 1\n\nline 3";
let expected = ">> line 1\n>> \n>> line 3";
assert_eq!(prefix(text, ">> "), expected);
}
#[test]
fn test_prefix_single_line() {
let text = "single line";
let expected = "PREFIX: single line";
assert_eq!(prefix(text, "PREFIX: "), expected);
}
#[test]
fn test_prefix_empty_string() {
assert_eq!(prefix("", "PREFIX: "), "");
}
#[test]
fn test_prefix_empty_prefix() {
let text = "line 1\nline 2";
assert_eq!(prefix(text, ""), text);
}
#[test]
fn test_dedent_and_prefix_combination() {
let indented = " def function():\n print('hello')\n return True";
let dedented = dedent(indented);
let prefixed = prefix(&dedented, ">>> ");
let expected = ">>> def function():\n>>> print('hello')\n>>> return True";
assert_eq!(prefixed, expected);
}
#[test]
fn test_dedent_real_code_example() {
let code = r#"
if condition:
for item in items:
process(item)
return result
else:
return None"#;
let dedented = dedent(code);
let expected = "\nif condition:\n for item in items:\n process(item)\n return result\nelse:\n return None";
assert_eq!(dedented, expected);
}
#[test]
fn test_prefix_code_comment() {
let code = "function main() {\n console.log('Hello');\n}";
let commented = prefix(code, "// ");
let expected = "// function main() {\n// console.log('Hello');\n// }";
assert_eq!(commented, expected);
}
#[test]
fn test_dedent_preserves_relative_indentation() {
let text = " start\n indented more\n back to start level\n indented again";
let dedented = dedent(text);
let expected = "start\n indented more\nback to start level\n indented again";
assert_eq!(dedented, expected);
}
#[test]
fn test_prefix_with_unicode() {
let text = "Hello 世界\nGoodbye 世界";
let prefixed = prefix(text, "🔹 ");
let expected = "🔹 Hello 世界\n🔹 Goodbye 世界";
assert_eq!(prefixed, expected);
}
#[test]
fn test_dedent_with_unicode() {
let text = " Hello 世界\n Goodbye 世界\n More indented 世界";
let dedented = dedent(text);
let expected = "Hello 世界\nGoodbye 世界\n More indented 世界";
assert_eq!(dedented, expected);
}

View File

@@ -0,0 +1,301 @@
//! Unit tests for text replacement functionality
//!
//! These tests validate the TextReplacer and TextReplacerBuilder including:
//! - Literal string replacement
//! - Regex pattern replacement
//! - Multiple chained replacements
//! - File operations (read, write, in-place)
//! - Error handling and edge cases
use sal_text::{TextReplacer, TextReplacerBuilder};
use std::fs;
use tempfile::NamedTempFile;
#[test]
fn test_text_replacer_literal_single() {
let replacer = TextReplacer::builder()
.pattern("hello")
.replacement("hi")
.regex(false)
.build()
.expect("Failed to build replacer");
let result = replacer.replace("hello world, hello universe");
assert_eq!(result, "hi world, hi universe");
}
#[test]
fn test_text_replacer_regex_single() {
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 and 456 more");
assert_eq!(result, "There are NUMBER items and NUMBER more");
}
#[test]
fn test_text_replacer_multiple_operations() {
let replacer = TextReplacer::builder()
.pattern(r"\d+")
.replacement("NUMBER")
.regex(true)
.and()
.pattern("world")
.replacement("universe")
.regex(false)
.build()
.expect("Failed to build replacer");
let result = replacer.replace("Hello world, there are 123 items");
assert_eq!(result, "Hello universe, there are NUMBER items");
}
#[test]
fn test_text_replacer_chained_operations() {
let replacer = TextReplacer::builder()
.pattern("cat")
.replacement("dog")
.regex(false)
.and()
.pattern("dog")
.replacement("animal")
.regex(false)
.build()
.expect("Failed to build replacer");
// Operations are applied in sequence, so "cat" -> "dog" -> "animal"
let result = replacer.replace("The cat sat on the mat");
assert_eq!(result, "The animal sat on the mat");
}
#[test]
fn test_text_replacer_regex_capture_groups() {
let replacer = TextReplacer::builder()
.pattern(r"(\d{4})-(\d{2})-(\d{2})")
.replacement("$3/$2/$1")
.regex(true)
.build()
.expect("Failed to build replacer");
let result = replacer.replace("Date: 2023-12-01");
assert_eq!(result, "Date: 01/12/2023");
}
#[test]
fn test_text_replacer_case_sensitive() {
let replacer = TextReplacer::builder()
.pattern("Hello")
.replacement("Hi")
.regex(false)
.build()
.expect("Failed to build replacer");
let result = replacer.replace("Hello world, hello universe");
assert_eq!(result, "Hi world, hello universe");
}
#[test]
fn test_text_replacer_regex_case_insensitive() {
let replacer = TextReplacer::builder()
.pattern(r"(?i)hello")
.replacement("Hi")
.regex(true)
.build()
.expect("Failed to build replacer");
let result = replacer.replace("Hello world, HELLO universe");
assert_eq!(result, "Hi world, Hi universe");
}
#[test]
fn test_text_replacer_empty_input() {
let replacer = TextReplacer::builder()
.pattern("test")
.replacement("replacement")
.regex(false)
.build()
.expect("Failed to build replacer");
let result = replacer.replace("");
assert_eq!(result, "");
}
#[test]
fn test_text_replacer_no_matches() {
let replacer = TextReplacer::builder()
.pattern("xyz")
.replacement("abc")
.regex(false)
.build()
.expect("Failed to build replacer");
let input = "Hello world";
let result = replacer.replace(input);
assert_eq!(result, input);
}
#[test]
fn test_text_replacer_file_operations() {
// Create a temporary file with test content
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
let test_content = "Hello world, there are 123 items";
fs::write(temp_file.path(), test_content).expect("Failed to write to temp file");
let replacer = TextReplacer::builder()
.pattern(r"\d+")
.replacement("NUMBER")
.regex(true)
.and()
.pattern("world")
.replacement("universe")
.regex(false)
.build()
.expect("Failed to build replacer");
// Test replace_file
let result = replacer.replace_file(temp_file.path()).expect("Failed to replace file content");
assert_eq!(result, "Hello universe, there are NUMBER items");
// Verify original file is unchanged
let original_content = fs::read_to_string(temp_file.path()).expect("Failed to read original file");
assert_eq!(original_content, test_content);
}
#[test]
fn test_text_replacer_file_in_place() {
// Create a temporary file with test content
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
let test_content = "Hello world, there are 123 items";
fs::write(temp_file.path(), test_content).expect("Failed to write to temp file");
let replacer = TextReplacer::builder()
.pattern("world")
.replacement("universe")
.regex(false)
.build()
.expect("Failed to build replacer");
// Test replace_file_in_place
replacer.replace_file_in_place(temp_file.path()).expect("Failed to replace file in place");
// Verify file content was changed
let new_content = fs::read_to_string(temp_file.path()).expect("Failed to read modified file");
assert_eq!(new_content, "Hello universe, there are 123 items");
}
#[test]
fn test_text_replacer_file_to_file() {
// Create source file
let mut source_file = NamedTempFile::new().expect("Failed to create source file");
let test_content = "Hello world, there are 123 items";
fs::write(source_file.path(), test_content).expect("Failed to write to source file");
// Create destination file
let dest_file = NamedTempFile::new().expect("Failed to create dest file");
let replacer = TextReplacer::builder()
.pattern(r"\d+")
.replacement("NUMBER")
.regex(true)
.build()
.expect("Failed to build replacer");
// Test replace_file_to
replacer.replace_file_to(source_file.path(), dest_file.path())
.expect("Failed to replace file to destination");
// Verify source file is unchanged
let source_content = fs::read_to_string(source_file.path()).expect("Failed to read source file");
assert_eq!(source_content, test_content);
// Verify destination file has replaced content
let dest_content = fs::read_to_string(dest_file.path()).expect("Failed to read dest file");
assert_eq!(dest_content, "Hello world, there are NUMBER items");
}
#[test]
fn test_text_replacer_invalid_regex() {
let result = TextReplacer::builder()
.pattern("[invalid regex")
.replacement("test")
.regex(true)
.build();
assert!(result.is_err());
}
#[test]
fn test_text_replacer_builder_default_regex_false() {
let replacer = TextReplacer::builder()
.pattern(r"\d+")
.replacement("NUMBER")
.build()
.expect("Failed to build replacer");
// Should treat as literal since regex defaults to false
let result = replacer.replace(r"Match \d+ pattern");
assert_eq!(result, "Match NUMBER pattern");
}
#[test]
fn test_text_replacer_complex_regex() {
let replacer = TextReplacer::builder()
.pattern(r"(\w+)@(\w+\.\w+)")
.replacement("EMAIL_ADDRESS")
.regex(true)
.build()
.expect("Failed to build replacer");
let result = replacer.replace("Contact john@example.com or jane@test.org");
assert_eq!(result, "Contact EMAIL_ADDRESS or EMAIL_ADDRESS");
}
#[test]
fn test_text_replacer_multiline_text() {
let replacer = TextReplacer::builder()
.pattern(r"^\s*//.*$")
.replacement("")
.regex(true)
.build()
.expect("Failed to build replacer");
let input = "function test() {\n // This is a comment\n return true;\n // Another comment\n}";
let result = replacer.replace(input);
// Note: This test depends on how the regex engine handles multiline mode
// The actual behavior might need adjustment based on regex flags
assert!(result.contains("function test()"));
assert!(result.contains("return true;"));
}
#[test]
fn test_text_replacer_unicode_text() {
let replacer = TextReplacer::builder()
.pattern("café")
.replacement("coffee")
.regex(false)
.build()
.expect("Failed to build replacer");
let result = replacer.replace("I love café in the morning");
assert_eq!(result, "I love coffee in the morning");
}
#[test]
fn test_text_replacer_large_text() {
let large_text = "word ".repeat(10000);
let replacer = TextReplacer::builder()
.pattern("word")
.replacement("term")
.regex(false)
.build()
.expect("Failed to build replacer");
let result = replacer.replace(&large_text);
assert_eq!(result, "term ".repeat(10000));
}