From abc9507b3f844144eb556f17cf5c98d2d8448518 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Fri, 13 Feb 2026 01:55:52 +0800 Subject: First --- src/fmt_case_style.rs | 382 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/fmt_path.rs | 232 ++++++++++++++++++++++++++++++ src/lib.rs | 48 +++++++ 3 files changed, 662 insertions(+) create mode 100644 src/fmt_case_style.rs create mode 100644 src/fmt_path.rs create mode 100644 src/lib.rs (limited to 'src') diff --git a/src/fmt_case_style.rs b/src/fmt_case_style.rs new file mode 100644 index 0000000..076bdd3 --- /dev/null +++ b/src/fmt_case_style.rs @@ -0,0 +1,382 @@ +pub struct CaseFormatter { + content: Vec, +} + +impl From for CaseFormatter { + fn from(value: String) -> Self { + Self { + content: str_split(value), + } + } +} + +impl From<&String> for CaseFormatter { + fn from(value: &String) -> Self { + Self { + content: str_split(value.clone()), + } + } +} + +impl From<&str> for CaseFormatter { + fn from(value: &str) -> Self { + Self { + content: str_split(value.to_string()), + } + } +} + +/// Split the string into segments for conversion +fn str_split(input: String) -> Vec { + 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); + + // Detect case boundaries: + // when the current character is lowercase and the next is uppercase (e.g., "bre[wC]offee") + // Treat as a word boundary in PascalCase or camelCase, insert a space + 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() +} + +impl CaseFormatter { + /// Convert to camelCase format (brewCoffee) + /// + /// # Examples + /// + /// ``` + /// # use just_fmt::fmt_case_style::CaseFormatter; + /// let processor = CaseFormatter::from("brew_coffee"); + /// assert_eq!(processor.to_camel_case(), "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::()); + result.push_str(&chars.collect::().to_lowercase()); + } + } + } + result + } + + /// Convert to PascalCase format (BrewCoffee) + /// + /// # Examples + /// + /// ``` + /// # use just_fmt::fmt_case_style::CaseFormatter; + /// let processor = CaseFormatter::from("brew_coffee"); + /// assert_eq!(processor.to_pascal_case(), "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::()); + result.push_str(&chars.collect::().to_lowercase()); + } + } + result + } + + /// Convert to kebab-case format (brew-coffee) + /// + /// # Examples + /// + /// ``` + /// # use just_fmt::fmt_case_style::CaseFormatter; + /// let processor = CaseFormatter::from("brew_coffee"); + /// assert_eq!(processor.to_kebab_case(), "brew-coffee"); + /// ``` + pub fn to_kebab_case(&self) -> String { + self.content.join("-").to_lowercase() + } + + /// Convert to snake_case format (brew_coffee) + /// + /// # Examples + /// + /// ``` + /// # use just_fmt::fmt_case_style::CaseFormatter; + /// let processor = CaseFormatter::from("brewCoffee"); + /// assert_eq!(processor.to_snake_case(), "brew_coffee"); + /// ``` + pub fn to_snake_case(&self) -> String { + self.content.join("_").to_lowercase() + } + + /// Convert to dot.case format (brew.coffee) + /// + /// # Examples + /// + /// ``` + /// # use just_fmt::fmt_case_style::CaseFormatter; + /// let processor = CaseFormatter::from("brew_coffee"); + /// assert_eq!(processor.to_dot_case(), "brew.coffee"); + /// ``` + pub fn to_dot_case(&self) -> String { + self.content.join(".").to_lowercase() + } + + /// Convert to Title Case format (Brew Coffee) + /// + /// # Examples + /// + /// ``` + /// # use just_fmt::fmt_case_style::CaseFormatter; + /// let processor = CaseFormatter::from("brew_coffee"); + /// assert_eq!(processor.to_title_case(), "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::()); + result.push_str(&chars.collect::().to_lowercase()); + } + result.push(' '); + } + result.pop(); + result + } + + /// Convert to lower case format (brew coffee) + /// + /// # Examples + /// + /// ``` + /// # use just_fmt::fmt_case_style::CaseFormatter; + /// let processor = CaseFormatter::from("BREW COFFEE"); + /// assert_eq!(processor.to_lower_case(), "brew coffee"); + /// ``` + pub fn to_lower_case(&self) -> String { + self.content.join(" ").to_lowercase() + } + + /// Convert to UPPER CASE format (BREW COFFEE) + /// + /// # Examples + /// + /// ``` + /// # use just_fmt::fmt_case_style::CaseFormatter; + /// let processor = CaseFormatter::from("brew coffee"); + /// assert_eq!(processor.to_upper_case(), "BREW COFFEE"); + /// ``` + pub fn to_upper_case(&self) -> String { + self.content.join(" ").to_uppercase() + } +} + +#[cfg(test)] +mod tests { + use crate::fmt_case_style::CaseFormatter; + + #[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 = CaseFormatter::from(input); + assert_eq!( + processor.to_camel_case(), + expected, + "Failed for input: '{}'", + input + ); + } + } + + #[test] + fn test_conversions() { + let processor = CaseFormatter::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"); + } +} + +/// Convert to camelCase format (brewCoffee) +/// +/// # Examples +/// +/// ``` +/// # use just_fmt::camel_case; +/// assert_eq!(camel_case!("brew_coffee"), "brewCoffee"); +/// ``` +#[macro_export] +macro_rules! camel_case { + ($input:expr) => {{ + use just_fmt::fmt_case_style::CaseFormatter; + CaseFormatter::from($input).to_camel_case() + }}; +} + +/// Convert to UPPER CASE format (BREW COFFEE) +/// +/// # Examples +/// +/// ``` +/// # use just_fmt::upper_case; +/// assert_eq!(upper_case!("brew coffee"), "BREW COFFEE"); +/// ``` +#[macro_export] +macro_rules! upper_case { + ($input:expr) => {{ + use just_fmt::fmt_case_style::CaseFormatter; + CaseFormatter::from($input).to_upper_case() + }}; +} + +/// Convert to lower case format (brew coffee) +/// +/// # Examples +/// +/// ``` +/// # use just_fmt::lower_case; +/// assert_eq!(lower_case!("BREW COFFEE"), "brew coffee"); +/// ``` +#[macro_export] +macro_rules! lower_case { + ($input:expr) => {{ + use just_fmt::fmt_case_style::CaseFormatter; + CaseFormatter::from($input).to_lower_case() + }}; +} + +/// Convert to Title Case format (Brew Coffee) +/// +/// # Examples +/// +/// ``` +/// # use just_fmt::title_case; +/// assert_eq!(title_case!("brew_coffee"), "Brew Coffee"); +/// ``` +#[macro_export] +macro_rules! title_case { + ($input:expr) => {{ + use just_fmt::fmt_case_style::CaseFormatter; + CaseFormatter::from($input).to_title_case() + }}; +} + +/// Convert to dot.case format (brew.coffee) +/// +/// # Examples +/// +/// ``` +/// # use just_fmt::dot_case; +/// assert_eq!(dot_case!("brew_coffee"), "brew.coffee"); +/// ``` +#[macro_export] +macro_rules! dot_case { + ($input:expr) => {{ + use just_fmt::fmt_case_style::CaseFormatter; + CaseFormatter::from($input).to_dot_case() + }}; +} + +/// Convert to snake_case format (brew_coffee) +/// +/// # Examples +/// +/// ``` +/// # use just_fmt::snake_case; +/// assert_eq!(snake_case!("brewCoffee"), "brew_coffee"); +/// ``` +#[macro_export] +macro_rules! snake_case { + ($input:expr) => {{ + use just_fmt::fmt_case_style::CaseFormatter; + CaseFormatter::from($input).to_snake_case() + }}; +} + +/// Convert to kebab-case format (brew-coffee) +/// +/// # Examples +/// +/// ``` +/// # use just_fmt::kebab_case; +/// assert_eq!(kebab_case!("brew_coffee"), "brew-coffee"); +/// ``` +#[macro_export] +macro_rules! kebab_case { + ($input:expr) => {{ + use just_fmt::fmt_case_style::CaseFormatter; + CaseFormatter::from($input).to_kebab_case() + }}; +} + +/// Convert to PascalCase format (BrewCoffee) +/// +/// # Examples +/// +/// ``` +/// # use just_fmt::pascal_case; +/// assert_eq!(pascal_case!("brew_coffee"), "BrewCoffee"); +/// ``` +#[macro_export] +macro_rules! pascal_case { + ($input:expr) => {{ + use just_fmt::fmt_case_style::CaseFormatter; + CaseFormatter::from($input).to_pascal_case() + }}; +} diff --git a/src/fmt_path.rs b/src/fmt_path.rs new file mode 100644 index 0000000..4b15fe4 --- /dev/null +++ b/src/fmt_path.rs @@ -0,0 +1,232 @@ +use std::path::{Path, PathBuf}; + +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +pub struct PathFormatConfig { + /// Whether to strip ANSI escape sequences (e.g., `\x1b[31m`, `\x1b[0m`). + /// When the path string may contain terminal color codes, enabling this option will clean them up. + pub strip_ansi: bool, + + /// Whether to strip characters disallowed in Windows filenames (`*`, `?`, `"`, `<`, `>`, `|`). + /// These characters typically have special meaning or are not allowed in filesystems. + pub strip_unfriendly_chars: bool, + + /// Whether to resolve parent directory references (`..`). + /// When enabled, attempts to navigate upward in the path, e.g., `/a/b/../c` becomes `/a/c`. + /// Note: This operation is based solely on the path string itself, without accessing the actual filesystem. + pub resolve_parent_dirs: bool, + + /// Whether to collapse consecutive forward slashes (`/`). + /// For example, `/home//user` becomes `/home/user`. + pub collapse_consecutive_slashes: bool, + + /// Whether to escape backslashes (`\`) to forward slashes (`/`). + /// This helps unify Windows‑style paths to Unix style, facilitating cross‑platform handling. + pub escape_backslashes: bool, +} + +impl Default for PathFormatConfig { + fn default() -> Self { + Self { + strip_ansi: true, + strip_unfriendly_chars: true, + resolve_parent_dirs: true, + collapse_consecutive_slashes: true, + escape_backslashes: true, + } + } +} + +/// Normalize an input path string into a canonical, platform‑agnostic form. +/// +/// This function removes ANSI escape sequences, unifies separators to `/`, +/// collapses duplicate slashes, strips unfriendly characters (`*`, `?`, `"`, `<`, `>`, `|`), +/// resolves simple `..` components, and preserves a trailing slash when present. +/// +/// See examples below for the exact normalization behavior. +/// +/// # Examples +/// +/// ``` +/// # use just_fmt::fmt_path::fmt_path_str; +/// # use just_fmt::fmt_path::FormatPathError; +/// +/// # fn main() -> Result<(), FormatPathError> { +/// assert_eq!(fmt_path_str("C:\\Users\\\\test")?, "C:/Users/test"); +/// assert_eq!( +/// fmt_path_str("/path/with/*unfriendly?chars")?, +/// "/path/with/unfriendlychars" +/// ); +/// assert_eq!(fmt_path_str("\x1b[31m/path\x1b[0m")?, "/path"); +/// assert_eq!(fmt_path_str("/home/user/dir/")?, "/home/user/dir/"); +/// assert_eq!( +/// fmt_path_str("/home/user/file.txt")?, +/// "/home/user/file.txt" +/// ); +/// assert_eq!( +/// fmt_path_str("/home/my_user/DOCS/JVCS_TEST/Workspace/../Vault/")?, +/// "/home/my_user/DOCS/JVCS_TEST/Vault/" +/// ); +/// assert_eq!(fmt_path_str("./home/file.txt")?, "home/file.txt"); +/// assert_eq!(fmt_path_str("./home/path/")?, "home/path/"); +/// assert_eq!(fmt_path_str("./")?, ""); +/// # Ok(()) +/// # } +/// ``` +pub fn fmt_path_str(path: impl Into) -> Result { + fmt_path_str_custom(path, &PathFormatConfig::default()) +} + +/// Normalize an input path string into a canonical, platform‑agnostic form. +/// +/// This function removes ANSI escape sequences, unifies separators to `/`, +/// collapses duplicate slashes, strips unfriendly characters (`*`, `?`, `"`, `<`, `>`, `|`), +/// resolves simple `..` components, and preserves a trailing slash when present. +/// +/// Unlike `fmt_path_str`, +/// this method uses `PathFormatConfig` to precisely control +/// what should be processed +pub fn fmt_path_str_custom( + path: impl Into, + config: &PathFormatConfig, +) -> Result { + let path_str = path.into(); + let ends_with_slash = path_str.ends_with('/'); + + // ANSI Strip + let cleaned = if config.strip_ansi { + strip_ansi_escapes::strip(&path_str) + } else { + path_str.as_bytes().to_vec() + }; + let path_without_ansi = String::from_utf8(cleaned).map_err(FormatPathError::InvalidUtf8)?; + + let path_with_forward_slash = if config.escape_backslashes { + path_without_ansi.replace('\\', "/") + } else { + path_without_ansi + }; + let mut result = String::new(); + let mut prev_char = '\0'; + + for c in path_with_forward_slash.chars() { + if config.collapse_consecutive_slashes && c == '/' && prev_char == '/' { + continue; + } + result.push(c); + prev_char = c; + } + + if config.strip_unfriendly_chars { + 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 = if config.resolve_parent_dirs { + normalize_path(&path_buf) + } else { + 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() + } +} + +/// Format a [`PathBuf`] into its canonical string form and convert it back. +/// +/// This is a convenience wrapper around [`fmt_path_str`], preserving +/// the semantics of [`PathBuf`] while applying the same normalization rules: +/// - normalize separators to `/` +/// - remove duplicated separators +/// - strip ANSI escape sequences +/// - remove unfriendly characters (`*`, `?`, etc.) +/// - resolve simple `..` segments +pub fn fmt_path(path: impl Into) -> Result { + let path_str = fmt_path_str(path.into().display().to_string())?; + Ok(PathBuf::from(path_str)) +} + +/// Format a [`PathBuf`] into its canonical string form and convert it back. +/// +/// Unlike `fmt_path`, +/// this method uses `PathFormatConfig` to precisely control +/// what should be processed +pub fn fmt_path_custom( + path: impl Into, + config: &PathFormatConfig, +) -> Result { + let path_str = fmt_path_str_custom(path.into().display().to_string(), config)?; + Ok(PathBuf::from(path_str)) +} + +/// Error type for path formatting operations. +#[derive(Debug)] +pub enum FormatPathError { + /// The input string contained invalid UTF-8 after stripping ANSI escape sequences. + InvalidUtf8(std::string::FromUtf8Error), +} + +impl std::fmt::Display for FormatPathError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + FormatPathError::InvalidUtf8(e) => { + write!(f, "Invalid UTF-8 after ANSI stripping: {}", e) + } + } + } +} + +impl std::error::Error for FormatPathError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + FormatPathError::InvalidUtf8(e) => Some(e), + } + } +} + +impl From for FormatPathError { + fn from(e: std::string::FromUtf8Error) -> Self { + FormatPathError::InvalidUtf8(e) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..6c486d7 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,48 @@ +/// Format naming styles +/// +/// Provides multiple naming style conversion functions, supporting conversion from input strings +/// in different formats to standardized word lists, then outputting to various common naming styles. +/// +/// # Main Features +/// +/// - Create `CaseFormatter` from a string +/// - Intelligently split input strings into word lists, handling multiple separators and case boundaries +/// - Convert to multiple naming formats: `camelCase`, `PascalCase`, `snake_case`, `kebab-case`, etc. +/// +/// # Examples +/// +/// ``` +/// # use just_fmt::fmt_case::CaseFormatter; +/// // Using CaseFormatter +/// let formatter = CaseFormatter::from("brew_coffee"); +/// assert_eq!(formatter.to_camel_case(), "brewCoffee"); +/// assert_eq!(formatter.to_pascal_case(), "BrewCoffee"); +/// assert_eq!(formatter.to_snake_case(), "brew_coffee"); +/// assert_eq!(formatter.to_kebab_case(), "brew-coffee"); +/// +/// // Using macros +/// # use just_fmt::fmt_case::{camel_case, pascal_case, snake_case, kebab_case} +/// assert_eq!(camel_case!("brew coffee"), "brewCoffee"); +/// assert_eq!(pascal_case!("brewCoffee"), "BrewCoffee"); +/// assert_eq!(snake_case!("brew_coffee"), "brew_coffee"); +/// assert_eq!(kebab_case!("brew.Coffee"), "brew-coffee"); +/// ``` +/// +/// # Supported Input Separators +/// +/// The module can recognize the following characters as word separators: +/// - Underscore `_` +/// - Comma `,` +/// - Dot `.` +/// - Hyphen `-` +/// - Space ` ` +/// +/// It can also automatically detect case boundaries (e.g., "camel" and "Case" in "camelCase") +pub mod fmt_case_style; + +/// Normalize an input path string into a canonical, platform‑agnostic form. +/// +/// This function removes ANSI escape sequences, unifies separators to `/`, +/// collapses duplicate slashes, strips unfriendly characters (`*`, `?`, `"`, `<`, `>`, `|`), +/// resolves simple `..` components, and preserves a trailing slash when present. +pub mod fmt_path; -- cgit