summaryrefslogtreecommitdiff
path: root/utils/string_proc/src
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-01-12 04:28:28 +0800
committer魏曹先生 <1992414357@qq.com>2026-01-12 04:51:34 +0800
commitc5fb22694e95f12c24b8d8af76999be7aea3fcec (patch)
tree399d8a24ce491fb635f3d09f2123290fe784059e /utils/string_proc/src
parent444754489aca0454eb54e15a49fb8a6db0b68a07 (diff)
Reorganize crate structure and move documentation files
Diffstat (limited to 'utils/string_proc/src')
-rw-r--r--utils/string_proc/src/format_path.rs111
-rw-r--r--utils/string_proc/src/format_processer.rs132
-rw-r--r--utils/string_proc/src/lib.rs50
-rw-r--r--utils/string_proc/src/macros.rs63
-rw-r--r--utils/string_proc/src/simple_processer.rs15
5 files changed, 371 insertions, 0 deletions
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()
+}