diff --git a/Cargo.toml b/Cargo.toml index a756d16..e9b2225 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ categories = ["os", "filesystem", "api-bindings"] readme = "README.md" [workspace] -members = [".", "vault", "git", "redisclient", "mycelium"] +members = [".", "vault", "git", "redisclient", "mycelium", "text"] [dependencies] hex = "0.4" @@ -63,6 +63,7 @@ futures = "0.3.30" sal-git = { path = "git" } sal-redisclient = { path = "redisclient" } sal-mycelium = { path = "mycelium" } +sal-text = { path = "text" } # Optional features for specific OS functionality [target.'cfg(unix)'.dependencies] diff --git a/src/lib.rs b/src/lib.rs index dd6dc22..700e01b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,7 +45,7 @@ pub mod postgresclient; pub mod process; pub use sal_redisclient as redisclient; pub mod rhai; -pub mod text; +pub use sal_text as text; pub mod vault; pub mod virt; pub mod zinit_client; diff --git a/src/rhai/mod.rs b/src/rhai/mod.rs index 9616863..3bea8b1 100644 --- a/src/rhai/mod.rs +++ b/src/rhai/mod.rs @@ -14,7 +14,6 @@ mod process; mod rfs; mod screen; -mod text; mod vault; mod zinit; @@ -101,19 +100,7 @@ pub use zinit::register_zinit_module; pub use sal_mycelium::rhai::register_mycelium_module; // Re-export text module -pub use text::register_text_module; -// Re-export text functions directly from text module -pub use crate::text::{ - // Dedent functions - dedent, - // Fix functions - name_fix, - path_fix, - prefix, -}; - -// Re-export TextReplacer functions -pub use text::*; +pub use sal_text::rhai::register_text_module; // Re-export crypto module pub use vault::register_crypto_module; @@ -166,7 +153,7 @@ pub fn register(engine: &mut Engine) -> Result<(), Box> { sal_mycelium::rhai::register_mycelium_module(engine)?; // Register Text module functions - text::register_text_module(engine)?; + sal_text::rhai::register_text_module(engine)?; // Register RFS module functions rfs::register(engine)?; diff --git a/src/text/README.md b/src/text/README.md deleted file mode 100644 index c3503cc..0000000 --- a/src/text/README.md +++ /dev/null @@ -1,307 +0,0 @@ -# SAL Text Module (`sal::text`) - -This module provides a collection of utilities for common text processing and manipulation tasks in Rust, with bindings for Rhai scripting. - -## Overview - -The `sal::text` module offers functionalities for: -- **Indentation**: Removing common leading whitespace (`dedent`) and adding prefixes to lines (`prefix`). -- **Normalization**: Sanitizing strings for use as filenames (`name_fix`) or fixing filename components within paths (`path_fix`). -- **Replacement**: A powerful `TextReplacer` for performing single or multiple regex or literal text replacements in strings or files. -- **Templating**: A `TemplateBuilder` using the Tera engine to render text templates with dynamic data. - -## Rust API - -### 1. Text Indentation - -Located in `src/text/dedent.rs` (for `dedent`) and `src/text/fix.rs` (likely contains `prefix`, though not explicitly confirmed by file view, its Rhai registration implies existence). - -- **`dedent(text: &str) -> String`**: Removes common leading whitespace from a multiline string. Tabs are treated as 4 spaces. Ideal for cleaning up heredocs or indented code snippets. - ```rust - use sal::text::dedent; - let indented_text = " Hello\n World"; - assert_eq!(dedent(indented_text), "Hello\n World"); - ``` - -- **`prefix(text: &str, prefix_str: &str) -> String`**: Adds `prefix_str` to the beginning of each line in `text`. - ```rust - use sal::text::prefix; - let text = "line1\nline2"; - assert_eq!(prefix(text, "> "), "> line1\n> line2"); - ``` - -### 2. Filename and Path Normalization - -Located in `src/text/fix.rs`. - -- **`name_fix(text: &str) -> String`**: Sanitizes a string to be suitable as a name or filename component. It converts to lowercase, replaces whitespace and various special characters with underscores, and removes non-ASCII characters. - ```rust - use sal::text::name_fix; - assert_eq!(name_fix("My File (New).txt"), "my_file_new_.txt"); - assert_eq!(name_fix("Café crème.jpg"), "caf_crm.jpg"); - ``` - -- **`path_fix(text: &str) -> String`**: Applies `name_fix` to the filename component of a given path string, leaving the directory structure intact. - ```rust - use sal::text::path_fix; - assert_eq!(path_fix("/some/path/My Document.docx"), "/some/path/my_document.docx"); - ``` - -### 3. Text Replacement (`TextReplacer`) - -Located in `src/text/replace.rs`. Provides `TextReplacer` and `TextReplacerBuilder`. - -The `TextReplacer` allows for complex, chained replacement operations on strings or file contents. - -**Builder Pattern:** - -```rust -use sal::text::TextReplacer; - -// Example: Multiple replacements, regex and literal -let replacer = TextReplacer::builder() - .pattern(r"\d+") // Regex: match one or more digits - .replacement("NUMBER") - .regex(true) - .and() // Chain another replacement - .pattern("World") // Literal string - .replacement("Universe") - .regex(false) // Explicitly literal, though default - .build() - .expect("Failed to build replacer"); - -let original_text = "Hello World, item 123 and item 456."; -let modified_text = replacer.replace(original_text); -assert_eq!(modified_text, "Hello Universe, item NUMBER and item NUMBER."); - -// Case-insensitive regex example -let case_replacer = TextReplacer::builder() - .pattern("apple") - .replacement("FRUIT") - .regex(true) - .case_insensitive(true) - .build() - .unwrap(); -assert_eq!(case_replacer.replace("Apple and apple"), "FRUIT and FRUIT"); -``` - -**Key `TextReplacerBuilder` methods:** -- `pattern(pat: &str)`: Sets the search pattern (string or regex). -- `replacement(rep: &str)`: Sets the replacement string. -- `regex(yes: bool)`: If `true`, treats `pattern` as a regex. Default is `false` (literal). -- `case_insensitive(yes: bool)`: If `true` (and `regex` is `true`), performs case-insensitive matching. -- `and()`: Finalizes the current replacement operation and prepares for a new one. -- `build()`: Consumes the builder and returns a `Result`. - -**`TextReplacer` methods:** -- `replace(input: &str) -> String`: Applies all configured replacements to the input string. -- `replace_file(path: P) -> io::Result`: Reads a file, applies replacements, returns the result. -- `replace_file_in_place(path: P) -> io::Result<()>`: Replaces content in the specified file directly. -- `replace_file_to(input_path: P1, output_path: P2) -> io::Result<()>`: Reads from `input_path`, applies replacements, writes to `output_path`. - -### 4. Text Templating (`TemplateBuilder`) - -Located in `src/text/template.rs`. Uses the Tera templating engine. - -**Builder Pattern:** - -```rust -use sal::text::TemplateBuilder; -use std::collections::HashMap; - -// Assume "./my_template.txt" contains: "Hello, {{ name }}! You are {{ age }}." - -// Create a temporary template file for the example -std::fs::write("./my_template.txt", "Hello, {{ name }}! You are {{ age }}.").unwrap(); - -let mut builder = TemplateBuilder::open("./my_template.txt").expect("Template not found"); - -// Add variables individually -builder = builder.add_var("name", "Alice").add_var("age", 30); - -let rendered_string = builder.render().expect("Rendering failed"); -assert_eq!(rendered_string, "Hello, Alice! You are 30."); - -// Or add multiple variables from a HashMap -let mut vars = HashMap::new(); -vars.insert("name", "Bob"); -vars.insert("age", "25"); // Values in HashMap are typically strings or serializable types - -let mut builder2 = TemplateBuilder::open("./my_template.txt").unwrap(); -builder2 = builder2.add_vars(vars); -let rendered_string2 = builder2.render().unwrap(); -assert_eq!(rendered_string2, "Hello, Bob! You are 25."); - -// Render directly to a file -// builder.render_to_file("output.txt").expect("Failed to write to file"); - -// Clean up temporary file -std::fs::remove_file("./my_template.txt").unwrap(); -``` - -**Key `TemplateBuilder` methods:** -- `open(template_path: P) -> io::Result`: Loads the template file. -- `add_var(name: S, value: V) -> Self`: Adds a single variable to the context. -- `add_vars(vars: HashMap) -> Self`: Adds multiple variables from a HashMap. -- `render() -> Result`: Renders the template to a string. -- `render_to_file(output_path: P) -> io::Result<()>`: Renders the template and writes it to the specified file. - -## Rhai Scripting with `herodo` - -The `sal::text` module's functionalities are exposed to Rhai scripts when using `herodo`. - -### Direct Functions - -- **`dedent(text_string)`**: Removes common leading whitespace. - - Example: `let clean_script = dedent(" if true {\n print(\"indented\");\n }");` -- **`prefix(text_string, prefix_string)`**: Adds `prefix_string` to each line of `text_string`. - - Example: `let prefixed_text = prefix("hello\nworld", "# ");` -- **`name_fix(text_string)`**: Normalizes a string for use as a filename. - - Example: `let filename = name_fix("My Document (V2).docx"); // "my_document_v2_.docx"` -- **`path_fix(path_string)`**: Normalizes the filename part of a path. - - Example: `let fixed_path = path_fix("/uploads/User Files/Report [Final].pdf");` - -### TextReplacer - -Provides text replacement capabilities through a builder pattern. - -1. **Create a builder**: `let builder = text_replacer_new();` -2. **Configure replacements** (methods return the builder for chaining): - - `builder = builder.pattern(search_pattern_string);` - - `builder = builder.replacement(replacement_string);` - - `builder = builder.regex(is_regex_bool);` (default `false`) - - `builder = builder.case_insensitive(is_case_insensitive_bool);` (default `false`, only applies if `regex` is `true`) - - `builder = builder.and();` (to add the current replacement and start a new one) -3. **Build the replacer**: `let replacer = builder.build();` -4. **Use the replacer**: - - `let modified_text = replacer.replace(original_text_string);` - - `let modified_text_from_file = replacer.replace_file(input_filepath_string);` - - `replacer.replace_file_in_place(filepath_string);` - - `replacer.replace_file_to(input_filepath_string, output_filepath_string);` - -### TemplateBuilder - -Provides text templating capabilities. - -1. **Open a template file**: `let tpl_builder = template_builder_open(template_filepath_string);` -2. **Add variables** (methods return the builder for chaining): - - `tpl_builder = tpl_builder.add_var(name_string, value);` (value can be string, int, float, bool, or array) - - `tpl_builder = tpl_builder.add_vars(map_object);` (map keys are variable names, values are their corresponding values) -3. **Render the template**: - - `let rendered_string = tpl_builder.render();` - - `tpl_builder.render_to_file(output_filepath_string);` - -## Rhai Example - -```rhai -// Create a temporary file for template demonstration -let template_content = "Report for {{user}}:\nItems processed: {{count}}.\nStatus: {{status}}."; -let template_path = "./temp_report_template.txt"; - -// Using file.write (assuming sal::file module is available and registered) -// For this example, we'll assume a way to write this file or that it exists. -// For a real script, ensure the file module is used or the file is pre-existing. -print(`Intending to write template to: ${template_path}`); -// In a real scenario: file.write(template_path, template_content); - -// For demonstration, let's simulate it exists for the template_builder_open call. -// If file module is not used, this script part needs adjustment or pre-existing file. - -// --- Text Normalization --- -let raw_filename = "User's Report [Draft 1].md"; -let safe_filename = name_fix(raw_filename); -print(`Safe filename: ${safe_filename}`); // E.g., "users_report_draft_1_.md" - -let raw_path = "/data/project files/Final Report (2023).pdf"; -let safe_path = path_fix(raw_path); -print(`Safe path: ${safe_path}`); // E.g., "/data/project files/final_report_2023_.pdf" - -// --- Dedent and Prefix --- -let script_block = "\n for item in items {\n print(item);\n }\n"; -let dedented_script = dedent(script_block); -print("Dedented script:\n" + dedented_script); - -let prefixed_log = prefix("Operation successful.\nDetails logged.", "LOG: "); -print(prefixed_log); - -// --- TextReplacer Example --- -let text_to_modify = "The quick brown fox jumps over the lazy dog. The dog was very lazy."; - -let replacer_builder = text_replacer_new() - .pattern("dog") - .replacement("cat") - .case_insensitive(true) // Replace 'dog', 'Dog', 'DOG', etc. - .and() - .pattern("lazy") - .replacement("energetic") - .regex(false); // This is the default, explicit for clarity - -let replacer = replacer_builder.build(); -let replaced_text = replacer.replace(text_to_modify); -print(`Replaced text: ${replaced_text}`); -// Expected: The quick brown fox jumps over the energetic cat. The cat was very energetic. - -// --- TemplateBuilder Example --- -// This part assumes 'temp_report_template.txt' was successfully created with content: -// "Report for {{user}}:\nItems processed: {{count}}.\nStatus: {{status}}." -// If not, template_builder_open will fail. For a robust script, check file existence or create it. - -// Create a dummy template file if it doesn't exist for the example to run -// This would typically be done using the file module, e.g. file.write() -// For simplicity here, we'll just print a message if it's missing. -// In a real script: if !file.exists(template_path) { file.write(template_path, template_content); } - -// Let's try to proceed assuming the template might exist or skip if not. -// A more robust script would handle the file creation explicitly. - -// For the sake of this example, let's create it directly if possible (conceptual) -// This is a placeholder for actual file writing logic. -// if (true) { // Simulate file creation for example purpose -// std.os.remove_file(template_path); // Clean up if exists -// let f = std.io.open(template_path, "w"); f.write(template_content); f.close(); -// } - -// Due to the sandbox, direct file system manipulation like above isn't typically done in Rhai examples -// without relying on registered SAL functions. We'll assume the file exists. - -print("Attempting to use template: " + template_path); -// It's better to ensure the file exists before calling template_builder_open -// For this example, we'll proceed, but in a real script, handle file creation. - -// Create a dummy file for the template example to work in isolation -// This is not ideal but helps for a self-contained example if file module isn't used prior. -// In a real SAL script, you'd use `file.write`. -let _dummy_template_file_path = "./example_template.rhai.tmp"; -// file.write(_dummy_template_file_path, "Name: {{name}}, Age: {{age}}"); - -// Using a known, simple template string for robustness if file ops are tricky in example context -let tpl_builder = template_builder_open(_dummy_template_file_path); // Use the dummy/known file - -if tpl_builder.is_ok() { - let mut template_engine = tpl_builder.unwrap(); - template_engine = template_engine.add_var("user", "Jane Doe"); - template_engine = template_engine.add_var("count", 150); - template_engine = template_engine.add_var("status", "Completed"); - - let report_output = template_engine.render(); - if report_output.is_ok() { - print("Generated Report:\n" + report_output.unwrap()); - } else { - print("Error rendering template: " + report_output.unwrap_err()); - } - - // Example: Render to file - // template_engine.render_to_file("./generated_report.txt"); - // print("Report also written to ./generated_report.txt"); -} else { - print("Skipping TemplateBuilder example as template file '" + _dummy_template_file_path + "' likely missing or unreadable."); - print("Error: " + tpl_builder.unwrap_err()); - print("To run this part, ensure '" + _dummy_template_file_path + "' exists with content like: 'Name: {{name}}, Age: {{age}}'"); -} - -// Clean up dummy file -// file.remove(_dummy_template_file_path); - -``` - -**Note on Rhai Example File Operations:** The Rhai example above includes comments about file creation for the `TemplateBuilder` part. In a real `herodo` script, you would use `sal::file` module functions (e.g., `file.write`, `file.exists`, `file.remove`) to manage the template file. For simplicity and to avoid making the example dependent on another module's full setup path, it highlights where such operations would occur. The example tries to use a dummy path and gracefully skips if the template isn't found, which is a common issue when running examples in restricted environments or without proper setup. The core logic of using `TemplateBuilder` once the template is loaded remains the same. \ No newline at end of file diff --git a/src/text/mod.rs b/src/text/mod.rs deleted file mode 100644 index 584aab4..0000000 --- a/src/text/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -mod dedent; -mod fix; -mod replace; -mod template; - -pub use dedent::*; -pub use fix::*; -pub use replace::*; -pub use template::*; \ No newline at end of file diff --git a/text/Cargo.toml b/text/Cargo.toml new file mode 100644 index 0000000..ccefbca --- /dev/null +++ b/text/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "sal-text" +version = "0.1.0" +edition = "2021" +authors = ["PlanetFirst "] +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" diff --git a/text/README.md b/text/README.md new file mode 100644 index 0000000..c998d11 --- /dev/null +++ b/text/README.md @@ -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 diff --git a/src/text/dedent.rs b/text/src/dedent.rs similarity index 100% rename from src/text/dedent.rs rename to text/src/dedent.rs diff --git a/src/text/fix.rs b/text/src/fix.rs similarity index 100% rename from src/text/fix.rs rename to text/src/fix.rs diff --git a/text/src/lib.rs b/text/src/lib.rs new file mode 100644 index 0000000..e02329e --- /dev/null +++ b/text/src/lib.rs @@ -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::*; diff --git a/src/text/replace.rs b/text/src/replace.rs similarity index 100% rename from src/text/replace.rs rename to text/src/replace.rs diff --git a/src/rhai/text.rs b/text/src/rhai.rs similarity index 87% rename from src/rhai/text.rs rename to text/src/rhai.rs index adb1b08..737f4c9 100644 --- a/src/rhai/text.rs +++ b/text/src/rhai.rs @@ -2,12 +2,9 @@ //! //! This module provides Rhai wrappers for the functions in the Text module. -use rhai::{Engine, EvalAltResult, Array, Map, Position}; +use crate::{TemplateBuilder, TextReplacer, TextReplacerBuilder}; +use rhai::{Array, Engine, EvalAltResult, Map, Position}; use std::collections::HashMap; -use crate::text::{ - TextReplacer, TextReplacerBuilder, - TemplateBuilder -}; /// Register Text module functions with the Rhai engine /// @@ -21,10 +18,10 @@ use crate::text::{ pub fn register_text_module(engine: &mut Engine) -> Result<(), Box> { // 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); @@ -32,16 +29,16 @@ pub fn register_text_module(engine: &mut Engine) -> Result<(), Box Result<(), Box Result<(), Box Result<(), Box> { // Register TextReplacerBuilder type engine.register_type_with_name::("TextReplacerBuilder"); - + // Register TextReplacer type engine.register_type_with_name::("TextReplacer"); - + // Register TemplateBuilder type engine.register_type_with_name::("TemplateBuilder"); - + Ok(()) } @@ -82,7 +79,7 @@ fn io_error_to_rhai_error(result: std::io::Result) -> Result(result: Result) -> Result(result: Result) -> Result> { - result.map_err(|e| { - Box::new(EvalAltResult::ErrorRuntime( - e.into(), - Position::NONE - )) - }) + result.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(e.into(), Position::NONE))) } // TextReplacer implementation @@ -153,12 +145,19 @@ pub fn replace_file(replacer: &mut TextReplacer, path: &str) -> Result Result<(), Box> { +pub fn replace_file_in_place( + replacer: &mut TextReplacer, + path: &str, +) -> Result<(), Box> { 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> { +pub fn replace_file_to( + replacer: &mut TextReplacer, + input_path: &str, + output_path: &str, +) -> Result<(), Box> { io_error_to_rhai_error(replacer.replace_file_to(input_path, output_path)) } @@ -192,10 +191,11 @@ pub fn add_var_bool(builder: TemplateBuilder, name: &str, value: bool) -> Templa /// 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 - let vec: Vec = array.iter() + let vec: Vec = array + .iter() .filter_map(|v| v.clone().into_string().ok()) .collect(); - + builder.add_var(name, vec) } @@ -203,13 +203,13 @@ pub fn add_var_array(builder: TemplateBuilder, name: &str, array: Array) -> Temp 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) } @@ -220,6 +220,9 @@ pub fn render(builder: &mut TemplateBuilder) -> Result Result<(), Box> { +pub fn render_to_file( + builder: &mut TemplateBuilder, + output_path: &str, +) -> Result<(), Box> { io_error_to_rhai_error(builder.render_to_file(output_path)) -} \ No newline at end of file +} diff --git a/src/text/template.rs b/text/src/template.rs similarity index 100% rename from src/text/template.rs rename to text/src/template.rs diff --git a/text/tests/rhai/run_all_tests.rhai b/text/tests/rhai/run_all_tests.rhai new file mode 100644 index 0000000..63e99e9 --- /dev/null +++ b/text/tests/rhai/run_all_tests.rhai @@ -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 ==="); diff --git a/text/tests/rhai_integration_tests.rs b/text/tests/rhai_integration_tests.rs new file mode 100644 index 0000000..06ae166 --- /dev/null +++ b/text/tests/rhai_integration_tests.rs @@ -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> = 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> = 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> = 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> = 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> = 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> = 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> = 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> = 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> = 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> = 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> = 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> = 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> = 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> = 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 + ); + } + } +} diff --git a/text/tests/string_normalization_tests.rs b/text/tests/string_normalization_tests.rs new file mode 100644 index 0000000..d6f899e --- /dev/null +++ b/text/tests/string_normalization_tests.rs @@ -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].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"); +} diff --git a/text/tests/template_tests.rs b/text/tests/template_tests.rs new file mode 100644 index 0000000..a762bcf --- /dev/null +++ b/text/tests/template_tests.rs @@ -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")); +} diff --git a/text/tests/text_indentation_tests.rs b/text/tests/text_indentation_tests.rs new file mode 100644 index 0000000..7ba5928 --- /dev/null +++ b/text/tests/text_indentation_tests.rs @@ -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); +} diff --git a/text/tests/text_replacement_tests.rs b/text/tests/text_replacement_tests.rs new file mode 100644 index 0000000..a07d582 --- /dev/null +++ b/text/tests/text_replacement_tests.rs @@ -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)); +}