summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-02-13 01:55:52 +0800
committer魏曹先生 <1992414357@qq.com>2026-02-13 02:03:50 +0800
commitabc9507b3f844144eb556f17cf5c98d2d8448518 (patch)
tree9d28d75fa8cc98152da29fb8bb8b556c1d67fee9 /src
First
Diffstat (limited to 'src')
-rw-r--r--src/fmt_case_style.rs382
-rw-r--r--src/fmt_path.rs232
-rw-r--r--src/lib.rs48
3 files changed, 662 insertions, 0 deletions
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<String>,
+}
+
+impl From<String> 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<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);
+
+ // 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::<String>());
+ result.push_str(&chars.collect::<String>().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::<String>());
+ result.push_str(&chars.collect::<String>().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::<String>());
+ result.push_str(&chars.collect::<String>().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<String>) -> Result<String, FormatPathError> {
+ 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<String>,
+ config: &PathFormatConfig,
+) -> Result<String, FormatPathError> {
+ 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<PathBuf>) -> Result<PathBuf, FormatPathError> {
+ 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<PathBuf>,
+ config: &PathFormatConfig,
+) -> Result<PathBuf, FormatPathError> {
+ 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<std::string::FromUtf8Error> 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;