Skip to main content

path_rs/
text.rs

1//! Generic text/path-token normalization (no repository or VCS semantics).
2//!
3//! Callers that need Git-specific rules (e.g. stripping `.git` suffixes) must
4//! compose those rules themselves on top of [`normalize_path_token`].
5
6/// How to transform letter case in a comparison key or token.
7///
8/// # Platform notes
9///
10/// - Windows is normally case-insensitive for filesystem paths.
11/// - Linux is normally case-sensitive.
12/// - macOS may be either, depending on the volume.
13/// - Case folding is **not** proof of filesystem identity.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15#[non_exhaustive]
16pub enum CaseNormalization {
17    /// Leave case unchanged.
18    #[default]
19    Preserve,
20    /// ASCII A–Z → a–z only.
21    AsciiLowercase,
22    /// Unicode lowercase mapping (`str::to_lowercase`).
23    UnicodeLowercase,
24    /// Platform-oriented default: ASCII lowercase on Windows; preserve elsewhere.
25    PlatformDefault,
26}
27
28impl CaseNormalization {
29    /// Apply this case policy to `value`.
30    pub fn apply(self, value: &str) -> String {
31        match self {
32            Self::Preserve => value.to_owned(),
33            Self::AsciiLowercase => value.to_ascii_lowercase(),
34            Self::UnicodeLowercase => value.to_lowercase(),
35            Self::PlatformDefault => {
36                if cfg!(windows) {
37                    value.to_ascii_lowercase()
38                } else {
39                    value.to_owned()
40                }
41            }
42        }
43    }
44}
45
46/// Options for generic path/token string normalization.
47///
48/// This is intentionally free of repository, Git, or product-specific rules.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct TextNormalizationOptions {
51    /// Trim leading and trailing Unicode whitespace.
52    pub trim_whitespace: bool,
53    /// Strip trailing `/` and `\` characters.
54    pub trim_trailing_separators: bool,
55    /// Rewrite `\` to `/` for separator-normalized comparison.
56    pub normalize_separators: bool,
57    /// Case policy for the result.
58    pub case: CaseNormalization,
59}
60
61impl Default for TextNormalizationOptions {
62    fn default() -> Self {
63        Self {
64            trim_whitespace: true,
65            trim_trailing_separators: true,
66            normalize_separators: true,
67            case: CaseNormalization::Preserve,
68        }
69    }
70}
71
72impl TextNormalizationOptions {
73    /// Create default options (`Self::default()`).
74    pub fn new() -> Self {
75        Self::default()
76    }
77}
78
79/// Normalize a path-like text token for comparison or matching.
80///
81/// # Filesystem access
82///
83/// **No.**
84///
85/// # Returns
86///
87/// A `String` suitable for comparison only — not necessarily a valid filesystem path.
88///
89/// # Examples
90///
91/// ```
92/// use path_rs::{normalize_path_token, CaseNormalization, TextNormalizationOptions};
93///
94/// let key = normalize_path_token(
95///     r"C:\Users\Floris\Repo\",
96///     &TextNormalizationOptions {
97///         trim_whitespace: true,
98///         trim_trailing_separators: true,
99///         normalize_separators: true,
100///         case: CaseNormalization::AsciiLowercase,
101///     },
102/// );
103/// assert_eq!(key, "c:/users/floris/repo");
104/// ```
105pub fn normalize_path_token(value: &str, options: &TextNormalizationOptions) -> String {
106    let mut s = if options.trim_whitespace {
107        value.trim()
108    } else {
109        value
110    }
111    .to_owned();
112
113    if options.trim_trailing_separators {
114        while s.ends_with('/') || s.ends_with('\\') {
115            s.pop();
116        }
117    }
118
119    if options.normalize_separators {
120        s = s.replace('\\', "/");
121        // Collapse repeated separators for token comparison (not for FS paths).
122        while s.contains("//") {
123            s = s.replace("//", "/");
124        }
125    }
126
127    options.case.apply(&s)
128}