Skip to main content

humanize_string/
lib.rs

1//! # humanize-string — make a machine string human-readable
2//!
3//! Convert a `camelCase`, `snake_case`, or `dash-case` string into a readable phrase:
4//! `fooBar` → `Foo bar`, `background-color` → `Background color`,
5//! `XMLHttpRequest` → `Xml http request`. A faithful Rust port of the
6//! [`humanize-string`](https://www.npmjs.com/package/humanize-string) npm package.
7//!
8//! ```
9//! use humanize_string::humanize_string;
10//!
11//! assert_eq!(humanize_string("fooBar"), "Foo bar");
12//! assert_eq!(humanize_string("background-color"), "Background color");
13//! assert_eq!(humanize_string("HELLO_WORLD"), "Hello world");
14//! ```
15//!
16//! Pass `preserve_case = true` to skip the camelCase splitting and keep the original casing
17//! (only whitespace is normalized and the first letter capitalized):
18//!
19//! ```
20//! use humanize_string::humanize_string_with;
21//! assert_eq!(humanize_string_with("fooBar", true), "FooBar");
22//! assert_eq!(humanize_string_with("HELLO_WORLD", true), "HELLO WORLD");
23//! ```
24
25#![forbid(unsafe_code)]
26#![doc(html_root_url = "https://docs.rs/humanize-string/0.1.0")]
27
28use decamelize::decamelize;
29
30// Compile-test the README's examples as part of `cargo test`.
31#[cfg(doctest)]
32#[doc = include_str!("../README.md")]
33struct ReadmeDoctests;
34
35/// Make `string` human-readable: split camelCase, replace `_`/`-` with spaces, collapse
36/// whitespace, and capitalize the first letter.
37///
38/// Equivalent to [`humanize_string_with(string, false)`](humanize_string_with).
39///
40/// ```
41/// # use humanize_string::humanize_string;
42/// assert_eq!(humanize_string("innerHTML"), "Inner html");
43/// ```
44#[must_use]
45pub fn humanize_string(string: &str) -> String {
46    humanize_string_with(string, false)
47}
48
49/// Make `string` human-readable, optionally preserving the original casing.
50///
51/// When `preserve_case` is `false` (the default in [`humanize_string`]), the input is first
52/// de-camelcased and lower-cased. When `true`, the casing is kept as-is and only whitespace
53/// normalization and the leading capitalization are applied.
54///
55/// ```
56/// # use humanize_string::humanize_string_with;
57/// assert_eq!(humanize_string_with("my_url_string", false), "My url string");
58/// ```
59#[must_use]
60pub fn humanize_string_with(string: &str, preserve_case: bool) -> String {
61    let mut s = if preserve_case {
62        string.to_string()
63    } else {
64        decamelize(string).to_lowercase()
65    };
66
67    s = replace_underscore_dash(&s);
68    s = collapse_whitespace(&s);
69    let s = js_trim(&s);
70    capitalize_first(s)
71}
72
73/// `/[_-]+/g → " "` — replace each run of underscores/dashes with a single space.
74fn replace_underscore_dash(s: &str) -> String {
75    let mut out = String::with_capacity(s.len());
76    let mut in_run = false;
77    for c in s.chars() {
78        if c == '_' || c == '-' {
79            if !in_run {
80                out.push(' ');
81                in_run = true;
82            }
83        } else {
84            out.push(c);
85            in_run = false;
86        }
87    }
88    out
89}
90
91/// `/\s{2,}/g → " "` — collapse each run of two-or-more whitespace into a single space
92/// (single whitespace characters are left unchanged, as in the reference).
93fn collapse_whitespace(s: &str) -> String {
94    let chars: Vec<char> = s.chars().collect();
95    let mut out = String::with_capacity(s.len());
96    let mut i = 0;
97    while i < chars.len() {
98        if is_js_whitespace(chars[i]) {
99            let start = i;
100            while i < chars.len() && is_js_whitespace(chars[i]) {
101                i += 1;
102            }
103            if i - start >= 2 {
104                out.push(' ');
105            } else {
106                out.push(chars[start]);
107            }
108        } else {
109            out.push(chars[i]);
110            i += 1;
111        }
112    }
113    out
114}
115
116/// `String.prototype.trim()` — trim JavaScript whitespace from both ends.
117fn js_trim(s: &str) -> &str {
118    s.trim_matches(is_js_whitespace)
119}
120
121/// `charAt(0).toUpperCase() + slice(1)` — uppercase the first character.
122fn capitalize_first(s: &str) -> String {
123    let mut chars = s.chars();
124    match chars.next() {
125        None => String::new(),
126        Some(first) => first.to_uppercase().chain(chars).collect(),
127    }
128}
129
130/// The set of characters matched by JavaScript's `\s` (and trimmed by `trim`).
131fn is_js_whitespace(c: char) -> bool {
132    matches!(
133        c,
134        '\u{0009}'
135            | '\u{000A}'
136            | '\u{000B}'
137            | '\u{000C}'
138            | '\u{000D}'
139            | '\u{0020}'
140            | '\u{00A0}'
141            | '\u{1680}'
142            | '\u{2000}'
143            ..='\u{200A}'
144                | '\u{2028}'
145                | '\u{2029}'
146                | '\u{202F}'
147                | '\u{205F}'
148                | '\u{3000}'
149                | '\u{FEFF}'
150    )
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn default_mode() {
159        assert_eq!(humanize_string("fooBar"), "Foo bar");
160        assert_eq!(humanize_string("background-color"), "Background color");
161        assert_eq!(humanize_string("_id"), "Id");
162        assert_eq!(humanize_string("fooBar123"), "Foo bar123");
163        assert_eq!(humanize_string("BowmoreIslay"), "Bowmore islay");
164        assert_eq!(humanize_string("XMLHttpRequest"), "Xml http request");
165        assert_eq!(humanize_string("my_url_string"), "My url string");
166        assert_eq!(humanize_string("HELLO_WORLD"), "Hello world");
167        assert_eq!(
168            humanize_string("backgroundColorURL"),
169            "Background color url"
170        );
171    }
172
173    #[test]
174    fn preserve_case() {
175        assert_eq!(humanize_string_with("fooBar", true), "FooBar");
176        assert_eq!(humanize_string_with("HELLO_WORLD", true), "HELLO WORLD");
177        assert_eq!(humanize_string_with("my-url", true), "My url");
178    }
179
180    #[test]
181    fn whitespace_and_sharp_s() {
182        assert_eq!(humanize_string("  spaced  out  "), "Spaced out");
183        assert_eq!(humanize_string("a_b-c__d"), "A b c d");
184        assert_eq!(humanize_string("ßeta"), "SSeta");
185    }
186
187    #[test]
188    fn edge_cases() {
189        assert_eq!(humanize_string(""), "");
190        assert_eq!(humanize_string("a"), "A");
191        assert_eq!(humanize_string("UPPERCASE"), "Uppercase");
192        assert_eq!(humanize_string("___"), "");
193    }
194}