1#![forbid(unsafe_code)]
26#![doc(html_root_url = "https://docs.rs/humanize-string/0.1.0")]
27
28use decamelize::decamelize;
29
30#[cfg(doctest)]
32#[doc = include_str!("../README.md")]
33struct ReadmeDoctests;
34
35#[must_use]
45pub fn humanize_string(string: &str) -> String {
46 humanize_string_with(string, false)
47}
48
49#[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
73fn 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
91fn 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
116fn js_trim(s: &str) -> &str {
118 s.trim_matches(is_js_whitespace)
119}
120
121fn 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
130fn 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}