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

View File

@@ -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;

View File

@@ -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<rhai::EvalAltResult>> {
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)?;

View File

@@ -1,225 +0,0 @@
//! Rhai wrappers for Text module functions
//!
//! This module provides Rhai wrappers for the functions in the Text module.
use rhai::{Engine, EvalAltResult, Array, Map, Position};
use std::collections::HashMap;
use crate::text::{
TextReplacer, TextReplacerBuilder,
TemplateBuilder
};
/// 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::text::name_fix);
engine.register_fn("path_fix", crate::text::path_fix);
// Register Dedent functions directly from text module
engine.register_fn("dedent", crate::text::dedent);
engine.register_fn("prefix", crate::text::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))
}

View File

@@ -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, String>`.
**`TextReplacer` methods:**
- `replace(input: &str) -> String`: Applies all configured replacements to the input string.
- `replace_file(path: P) -> io::Result<String>`: 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<Self>`: Loads the template file.
- `add_var(name: S, value: V) -> Self`: Adds a single variable to the context.
- `add_vars(vars: HashMap<S, V>) -> Self`: Adds multiple variables from a HashMap.
- `render() -> Result<String, tera::Error>`: 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.

View File

@@ -1,137 +0,0 @@
/**
* 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");
}
}

View File

@@ -1,99 +0,0 @@
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");
}
}

View File

@@ -1,9 +0,0 @@
mod dedent;
mod fix;
mod replace;
mod template;
pub use dedent::*;
pub use fix::*;
pub use replace::*;
pub use template::*;

View File

@@ -1,292 +0,0 @@
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(())
}
}

View File

@@ -1,310 +0,0 @@
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(())
}
}