diff options
Diffstat (limited to 'utils/string_proc')
| -rw-r--r-- | utils/string_proc/Cargo.toml | 7 | ||||
| -rw-r--r-- | utils/string_proc/src/format_path.rs | 111 | ||||
| -rw-r--r-- | utils/string_proc/src/format_processer.rs | 132 | ||||
| -rw-r--r-- | utils/string_proc/src/lib.rs | 50 | ||||
| -rw-r--r-- | utils/string_proc/src/macros.rs | 63 | ||||
| -rw-r--r-- | utils/string_proc/src/simple_processer.rs | 15 |
6 files changed, 378 insertions, 0 deletions
diff --git a/utils/string_proc/Cargo.toml b/utils/string_proc/Cargo.toml new file mode 100644 index 0000000..5292339 --- /dev/null +++ b/utils/string_proc/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "string_proc" +version = "0.1.0" +edition = "2024" + +[dependencies] +strip-ansi-escapes = "0.2.1" diff --git a/utils/string_proc/src/format_path.rs b/utils/string_proc/src/format_path.rs new file mode 100644 index 0000000..35689b8 --- /dev/null +++ b/utils/string_proc/src/format_path.rs @@ -0,0 +1,111 @@ +use std::path::{Path, PathBuf}; + +/// Format path str +pub fn format_path_str(path: impl Into<String>) -> Result<String, std::io::Error> { + let path_str = path.into(); + let ends_with_slash = path_str.ends_with('/'); + + // ANSI Strip + let cleaned = strip_ansi_escapes::strip(&path_str); + let path_without_ansi = String::from_utf8(cleaned) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + + let path_with_forward_slash = path_without_ansi.replace('\\', "/"); + let mut result = String::new(); + let mut prev_char = '\0'; + + for c in path_with_forward_slash.chars() { + if c == '/' && prev_char == '/' { + continue; + } + result.push(c); + prev_char = c; + } + + let unfriendly_chars = ['*', '?', '"', '<', '>', '|']; + result = result + .chars() + .filter(|c| !unfriendly_chars.contains(c)) + .collect(); + + // Handle ".." path components + let path_buf = PathBuf::from(&result); + let normalized_path = normalize_path(&path_buf); + result = normalized_path.to_string_lossy().replace('\\', "/"); + + // Restore trailing slash if original path had one + if ends_with_slash && !result.ends_with('/') { + result.push('/'); + } + + // Special case: when result is only "./", return "" + if result == "./" { + return Ok(String::new()); + } + + Ok(result) +} + +/// Normalize path by resolving ".." components without requiring file system access +fn normalize_path(path: &Path) -> PathBuf { + let mut components = Vec::new(); + + for component in path.components() { + match component { + std::path::Component::ParentDir => { + if !components.is_empty() { + components.pop(); + } + } + std::path::Component::CurDir => { + // Skip current directory components + } + _ => { + components.push(component); + } + } + } + + if components.is_empty() { + PathBuf::from(".") + } else { + components.iter().collect() + } +} + +pub fn format_path(path: impl Into<PathBuf>) -> Result<PathBuf, std::io::Error> { + let path_str = format_path_str(path.into().display().to_string())?; + Ok(PathBuf::from(path_str)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_format_path() -> Result<(), std::io::Error> { + assert_eq!(format_path_str("C:\\Users\\\\test")?, "C:/Users/test"); + + assert_eq!( + format_path_str("/path/with/*unfriendly?chars")?, + "/path/with/unfriendlychars" + ); + + assert_eq!(format_path_str("\x1b[31m/path\x1b[0m")?, "/path"); + assert_eq!(format_path_str("/home/user/dir/")?, "/home/user/dir/"); + assert_eq!( + format_path_str("/home/user/file.txt")?, + "/home/user/file.txt" + ); + assert_eq!( + format_path_str("/home/my_user/DOCS/JVCS_TEST/Workspace/../Vault/")?, + "/home/my_user/DOCS/JVCS_TEST/Vault/" + ); + + assert_eq!(format_path_str("./home/file.txt")?, "home/file.txt"); + assert_eq!(format_path_str("./home/path/")?, "home/path/"); + assert_eq!(format_path_str("./")?, ""); + + Ok(()) + } +} diff --git a/utils/string_proc/src/format_processer.rs b/utils/string_proc/src/format_processer.rs new file mode 100644 index 0000000..8d0a770 --- /dev/null +++ b/utils/string_proc/src/format_processer.rs @@ -0,0 +1,132 @@ +pub struct FormatProcesser { + content: Vec<String>, +} + +impl From<String> for FormatProcesser { + fn from(value: String) -> Self { + Self { + content: Self::process_string(value), + } + } +} + +impl From<&str> for FormatProcesser { + fn from(value: &str) -> Self { + Self { + content: Self::process_string(value.to_string()), + } + } +} + +impl FormatProcesser { + /// Process the string into an intermediate format + fn process_string(input: String) -> Vec<String> { + let mut result = String::new(); + let mut prev_space = false; + + for c in input.chars() { + match c { + 'a'..='z' | 'A'..='Z' | '0'..='9' => { + result.push(c); + prev_space = false; + } + '_' | ',' | '.' | '-' | ' ' => { + if !prev_space { + result.push(' '); + prev_space = true; + } + } + _ => {} + } + } + + let mut processed = String::new(); + let mut chars = result.chars().peekable(); + + while let Some(c) = chars.next() { + processed.push(c); + if let Some(&next) = chars.peek() + && c.is_lowercase() + && next.is_uppercase() + { + processed.push(' '); + } + } + + processed + .to_lowercase() + .split_whitespace() + .map(|s| s.to_string()) + .collect() + } + + /// Convert to camelCase format (brewCoffee) + pub fn to_camel_case(&self) -> String { + let mut result = String::new(); + for (i, word) in self.content.iter().enumerate() { + if i == 0 { + result.push_str(&word.to_lowercase()); + } else { + let mut chars = word.chars(); + if let Some(first) = chars.next() { + result.push_str(&first.to_uppercase().collect::<String>()); + result.push_str(&chars.collect::<String>().to_lowercase()); + } + } + } + result + } + + /// Convert to PascalCase format (BrewCoffee) + pub fn to_pascal_case(&self) -> String { + let mut result = String::new(); + for word in &self.content { + let mut chars = word.chars(); + if let Some(first) = chars.next() { + result.push_str(&first.to_uppercase().collect::<String>()); + result.push_str(&chars.collect::<String>().to_lowercase()); + } + } + result + } + + /// Convert to kebab-case format (brew-coffee) + pub fn to_kebab_case(&self) -> String { + self.content.join("-").to_lowercase() + } + + /// Convert to snake_case format (brew_coffee) + pub fn to_snake_case(&self) -> String { + self.content.join("_").to_lowercase() + } + + /// Convert to dot.case format (brew.coffee) + pub fn to_dot_case(&self) -> String { + self.content.join(".").to_lowercase() + } + + /// Convert to Title Case format (Brew Coffee) + pub fn to_title_case(&self) -> String { + let mut result = String::new(); + for word in &self.content { + let mut chars = word.chars(); + if let Some(first) = chars.next() { + result.push_str(&first.to_uppercase().collect::<String>()); + result.push_str(&chars.collect::<String>().to_lowercase()); + } + result.push(' '); + } + result.pop(); + result + } + + /// Convert to lower case format (brew coffee) + pub fn to_lower_case(&self) -> String { + self.content.join(" ").to_lowercase() + } + + /// Convert to UPPER CASE format (BREW COFFEE) + pub fn to_upper_case(&self) -> String { + self.content.join(" ").to_uppercase() + } +} diff --git a/utils/string_proc/src/lib.rs b/utils/string_proc/src/lib.rs new file mode 100644 index 0000000..76588c1 --- /dev/null +++ b/utils/string_proc/src/lib.rs @@ -0,0 +1,50 @@ +pub mod format_path; +pub mod format_processer; +pub mod macros; +pub mod simple_processer; + +#[cfg(test)] +mod tests { + use crate::format_processer::FormatProcesser; + + #[test] + fn test_processer() { + let test_cases = vec![ + ("brew_coffee", "brewCoffee"), + ("brew, coffee", "brewCoffee"), + ("brew-coffee", "brewCoffee"), + ("Brew.Coffee", "brewCoffee"), + ("bRewCofFee", "bRewCofFee"), + ("brewCoffee", "brewCoffee"), + ("b&rewCoffee", "brewCoffee"), + ("BrewCoffee", "brewCoffee"), + ("brew.coffee", "brewCoffee"), + ("Brew_Coffee", "brewCoffee"), + ("BREW COFFEE", "brewCoffee"), + ]; + + for (input, expected) in test_cases { + let processor = FormatProcesser::from(input); + assert_eq!( + processor.to_camel_case(), + expected, + "Failed for input: '{}'", + input + ); + } + } + + #[test] + fn test_conversions() { + let processor = FormatProcesser::from("brewCoffee"); + + assert_eq!(processor.to_upper_case(), "BREW COFFEE"); + assert_eq!(processor.to_lower_case(), "brew coffee"); + assert_eq!(processor.to_title_case(), "Brew Coffee"); + assert_eq!(processor.to_dot_case(), "brew.coffee"); + assert_eq!(processor.to_snake_case(), "brew_coffee"); + assert_eq!(processor.to_kebab_case(), "brew-coffee"); + assert_eq!(processor.to_pascal_case(), "BrewCoffee"); + assert_eq!(processor.to_camel_case(), "brewCoffee"); + } +} diff --git a/utils/string_proc/src/macros.rs b/utils/string_proc/src/macros.rs new file mode 100644 index 0000000..135268e --- /dev/null +++ b/utils/string_proc/src/macros.rs @@ -0,0 +1,63 @@ +#[macro_export] +macro_rules! camel_case { + ($input:expr) => {{ + use string_proc::format_processer::FormatProcesser; + FormatProcesser::from($input).to_camel_case() + }}; +} + +#[macro_export] +macro_rules! upper_case { + ($input:expr) => {{ + use string_proc::format_processer::FormatProcesser; + FormatProcesser::from($input).to_upper_case() + }}; +} + +#[macro_export] +macro_rules! lower_case { + ($input:expr) => {{ + use string_proc::format_processer::FormatProcesser; + FormatProcesser::from($input).to_lower_case() + }}; +} + +#[macro_export] +macro_rules! title_case { + ($input:expr) => {{ + use string_proc::format_processer::FormatProcesser; + FormatProcesser::from($input).to_title_case() + }}; +} + +#[macro_export] +macro_rules! dot_case { + ($input:expr) => {{ + use string_proc::format_processer::FormatProcesser; + FormatProcesser::from($input).to_dot_case() + }}; +} + +#[macro_export] +macro_rules! snake_case { + ($input:expr) => {{ + use string_proc::format_processer::FormatProcesser; + FormatProcesser::from($input).to_snake_case() + }}; +} + +#[macro_export] +macro_rules! kebab_case { + ($input:expr) => {{ + use string_proc::format_processer::FormatProcesser; + FormatProcesser::from($input).to_kebab_case() + }}; +} + +#[macro_export] +macro_rules! pascal_case { + ($input:expr) => {{ + use string_proc::format_processer::FormatProcesser; + FormatProcesser::from($input).to_pascal_case() + }}; +} diff --git a/utils/string_proc/src/simple_processer.rs b/utils/string_proc/src/simple_processer.rs new file mode 100644 index 0000000..2de5dfc --- /dev/null +++ b/utils/string_proc/src/simple_processer.rs @@ -0,0 +1,15 @@ +/// Sanitizes a file path by replacing special characters with underscores. +/// +/// This function takes a file path as input and returns a sanitized version +/// where characters that are not allowed in file paths (such as path separators +/// and other reserved characters) are replaced with underscores. +pub fn sanitize_file_path<P: AsRef<str>>(path: P) -> String { + let path_str = path.as_ref(); + path_str + .chars() + .map(|c| match c { + '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_', + _ => c, + }) + .collect() +} |
