1+ //! Case conversion utilities for strings (PascalCase, camelCase, kebab-case, etc.).
2+
13use urlogger:: { LogLevel , log} ;
24
5+ /// Default characters that split a string into words (e.g. `foo-bar`, `foo_bar`).
36const STR_SPLITTERS : & [ char ] = & [ '-' , '_' , '/' , '.' ] ;
47
8+ /// Returns whether `c` is uppercase. Returns `None` for ASCII digits.
59pub fn is_uppercase ( c : char ) -> Option < bool > {
610 if c. is_ascii_digit ( ) {
711 return None ;
@@ -10,6 +14,10 @@ pub fn is_uppercase(c: char) -> Option<bool> {
1014 Some ( c != lower)
1115}
1216
17+ /// Splits a string into words at case boundaries and separators.
18+ ///
19+ /// Handles camelCase, PascalCase, SCREAMING_SNAKE, etc. Uses `separators` (or
20+ /// `STR_SPLITTERS`) to split. Empty segments are preserved (e.g. `foo--bar` → `["foo","","bar"]`).
1321pub fn split_by_case ( s : & str , separators : Option < & [ char ] > ) -> Vec < String > {
1422 let splitters: std:: collections:: HashSet < char > = separators
1523 . unwrap_or ( STR_SPLITTERS )
@@ -37,7 +45,7 @@ pub fn split_by_case(s: &str, separators: Option<&[char]>) -> Vec<String> {
3745
3846 let is_upper = is_uppercase ( c) ;
3947 if previous_splitter == Some ( false ) {
40- // Case rising edge: 小写 -> 大写 (e.g. camel|Case)
48+ // Case rising edge: lower -> upper (e.g. camel|Case)
4149 if previous_upper == Some ( false ) && is_upper == Some ( true ) {
4250 if !buff. is_empty ( ) {
4351 parts. push ( std:: mem:: take ( & mut buff) ) ;
@@ -47,12 +55,12 @@ pub fn split_by_case(s: &str, separators: Option<&[char]>) -> Vec<String> {
4755 previous_splitter = Some ( false ) ;
4856 continue ;
4957 }
50- // Case falling edge: 大写 -> 小写,且 buffer > 1 (e.g. AB|c -> A, Bc)
58+ // Case falling edge: upper -> lower, buffer.len() > 1 (e.g. AB|c → A, Bc)
5159 if previous_upper == Some ( true ) && is_upper == Some ( false ) {
5260 let char_count = buff. chars ( ) . count ( ) ;
5361 if char_count > 1 {
5462 let last_char = buff. chars ( ) . last ( ) . unwrap ( ) ;
55- // Byte index of the start of the last character
63+ // Byte index where the last character starts
5664 let new_len = buff
5765 . char_indices ( )
5866 . nth ( char_count - 1 )
@@ -82,6 +90,7 @@ pub fn split_by_case(s: &str, separators: Option<&[char]>) -> Vec<String> {
8290 parts
8391}
8492
93+ /// Capitalizes the first character; rest unchanged.
8594pub fn upper_first ( s : & str ) -> String {
8695 let mut chars = s. chars ( ) ;
8796 if let Some ( c) = chars. next ( ) {
@@ -91,6 +100,7 @@ pub fn upper_first(s: &str) -> String {
91100 }
92101}
93102
103+ /// Lowercases the first character; rest unchanged.
94104pub fn lower_first ( s : & str ) -> String {
95105 let mut chars = s. chars ( ) ;
96106 if let Some ( c) = chars. next ( ) {
@@ -100,7 +110,9 @@ pub fn lower_first(s: &str) -> String {
100110 }
101111}
102112
103- /// Convert a string to PascalCase.
113+ /// Converts a string to PascalCase.
114+ ///
115+ /// With `normalize == true`, each part is lowercased before capitalizing.
104116pub fn pascal_case ( s : & str , normalize : bool ) -> String {
105117 if s. is_empty ( ) {
106118 return String :: new ( ) ;
@@ -117,32 +129,31 @@ pub fn pascal_case(s: &str, normalize: bool) -> String {
117129 . collect ( )
118130}
119131
120- /// Convert a string to camelCase.
121- /// Uses `lower_first(pascal_case(s, normalize))` to match scule behavior.
132+ /// Converts a string to camelCase.
133+ ///
134+ /// Uses `lower_first(pascal_case(s, normalize))`.
122135pub fn camel_case ( s : & str , normalize : bool ) -> String {
123136 lower_first ( & pascal_case ( s, normalize) )
124137}
125138
126- /// Convert a string to kebab-case.
127- /// Splits by case, lowercases each part, joins with "-". Matches scule kebabCase.
139+ /// Converts a string to kebab-case (lowercase parts joined with `-`).
128140pub fn kebab_case ( s : & str ) -> String {
129141 lower_case_join ( s, "-" )
130142}
131143
132- /// Convert a string to snake_case.
133- /// Same as kebab_case but joins with "_". Matches scule snakeCase.
144+ /// Converts a string to snake_case (lowercase parts joined with `_`).
134145pub fn snake_case ( s : & str ) -> String {
135146 lower_case_join ( s, "_" )
136147}
137148
138- /// Convert a string to flatcase (all lowercase, no separators).
139- /// Same as kebab_case but joins with "". Matches scule flatCase.
149+ /// Converts a string to flatcase (all lowercase, no separators).
140150pub fn flat_case ( s : & str ) -> String {
141151 lower_case_join ( s, "" )
142152}
143153
144- /// Convert a string to Train-Case (each word capitalized, joined by "-").
145- /// Matches scule trainCase. With normalize, lowercases each part before capitalizing.
154+ /// Converts a string to Train-Case (each word capitalized, joined by `-`).
155+ ///
156+ /// With `normalize == true`, lowercases each part before capitalizing.
146157pub fn train_case ( s : & str , normalize : bool ) -> String {
147158 split_by_case ( s, None )
148159 . into_iter ( )
@@ -158,6 +169,7 @@ pub fn train_case(s: &str, normalize: bool) -> String {
158169 . join ( "-" )
159170}
160171
172+ /// Splits by case, lowercases each part, joins with `joiner`.
161173fn lower_case_join ( s : & str , joiner : & str ) -> String {
162174 split_by_case ( s, None )
163175 . into_iter ( )
@@ -166,14 +178,15 @@ fn lower_case_join(s: &str, joiner: &str) -> String {
166178 . join ( joiner)
167179}
168180
169- /// Words that stay lowercase in title case (a, an, and, as, at, but, by, for, if, in, is, nor, of, on, or, the, to, with)
181+ /// Minor words that remain lowercase in Title Case.
170182const TITLE_CASE_EXCEPTIONS : & [ & str ] = & [
171183 "a" , "an" , "and" , "as" , "at" , "but" , "by" , "for" , "if" , "in" , "is" , "nor" , "of" , "on" , "or" ,
172184 "the" , "to" , "with" ,
173185] ;
174186
175- /// Convert a string to Title Case (like train-case but with spaces, minor words lowercase).
176- /// Matches scule titleCase. With normalize, lowercases each part before capitalizing (except exceptions).
187+ /// Converts a string to Title Case (like train-case but with spaces, minor words lowercase).
188+ ///
189+ /// With `normalize == true`, lowercases each part before capitalizing (except `TITLE_CASE_EXCEPTIONS`).
177190pub fn title_case ( s : & str , normalize : bool ) -> String {
178191 split_by_case ( s, None )
179192 . into_iter ( )
@@ -192,6 +205,7 @@ pub fn title_case(s: &str, normalize: bool) -> String {
192205 . join ( " " )
193206}
194207
208+ /// Returns a greeting string. Example: `hello("world")` → `"Hello, world!"`.
195209pub fn hello ( name : & str ) -> String {
196210 log ! ( LogLevel :: Info , "lib.rs" ) ;
197211 format ! ( "Hello, {}!" , name)
@@ -257,7 +271,6 @@ mod tests {
257271
258272 #[ test]
259273 fn test_train_case ( ) {
260- // Same as scule: trainCase(input) - without normalize
261274 assert_eq ! ( train_case( "" , false ) , "" ) ;
262275 assert_eq ! ( train_case( "f" , false ) , "F" ) ;
263276 assert_eq ! ( train_case( "foo" , false ) , "Foo" ) ;
@@ -269,15 +282,13 @@ mod tests {
269282 assert_eq ! ( train_case( "WWW-authenticate" , false ) , "WWW-Authenticate" ) ;
270283 assert_eq ! ( train_case( "WWWAuthenticate" , false ) , "WWW-Authenticate" ) ;
271284
272- // Same as scule: trainCase(input, { normalize: true })
273285 assert_eq ! ( train_case( "AcceptCH" , true ) , "Accept-Ch" ) ;
274286 assert_eq ! ( train_case( "FOO_BAR" , true ) , "Foo-Bar" ) ;
275287 assert_eq ! ( train_case( "WWW-authenticate" , true ) , "Www-Authenticate" ) ;
276288 }
277289
278290 #[ test]
279291 fn test_title_case ( ) {
280- // Same as scule: titleCase(input) - without normalize
281292 assert_eq ! ( title_case( "" , false ) , "" ) ;
282293 assert_eq ! ( title_case( "f" , false ) , "F" ) ;
283294 assert_eq ! ( title_case( "foo" , false ) , "Foo" ) ;
0 commit comments