1#![allow(unsafe_code)]
2
3use core::ops::Range;
4
5use crate::__ctfe::StrBuf;
6use crate::slice::subslice;
7
8#[derive(Clone, Copy)]
9#[repr(u8)]
10enum TokenKind {
11 NonAscii = 1,
12 Lower = 2,
13 Upper = 3,
14 Digit = 4,
15 Dot = 5,
16 Other = 6,
17}
18
19impl TokenKind {
20 const fn new(b: u8) -> Self {
21 if !b.is_ascii() {
22 return TokenKind::NonAscii;
23 }
24 if b.is_ascii_lowercase() {
25 return TokenKind::Lower;
26 }
27 if b.is_ascii_uppercase() {
28 return TokenKind::Upper;
29 }
30 if b.is_ascii_digit() {
31 return TokenKind::Digit;
32 }
33 if b == b'.' {
34 return TokenKind::Dot;
35 }
36 TokenKind::Other
37 }
38
39 const fn is_boundary_word(s: &[u8]) -> bool {
40 let mut i = 0;
41 while i < s.len() {
42 let kind = Self::new(s[i]);
43 match kind {
44 TokenKind::Other | TokenKind::Dot => {}
45 _ => return false,
46 }
47 i += 1;
48 }
49 true
50 }
51}
52
53#[derive(Debug)]
54struct Boundaries<const N: usize> {
55 buf: [usize; N],
56 len: usize,
57}
58
59impl<const N: usize> Boundaries<N> {
60 const fn new(src: &str) -> Self {
61 let s = src.as_bytes();
62 assert!(s.len() + 1 == N);
63
64 let mut buf = [0; N];
65 let mut pos = 0;
66
67 macro_rules! push {
68 ($x: expr) => {{
69 buf[pos] = $x;
70 pos += 1;
71 }};
72 }
73
74 let mut k2: Option<TokenKind> = None;
75 let mut k1: Option<TokenKind> = None;
76
77 let mut i = 0;
78 while i < s.len() {
79 let b = s[i];
80 let k0 = TokenKind::new(b);
81
82 use TokenKind::*;
83
84 match (k1, k0) {
85 (None, _) => push!(i),
86 (Some(k1), k0) => {
87 if k1 as u8 != k0 as u8 {
88 match (k1, k0) {
89 (Upper, Lower) => push!(i - 1),
90 (NonAscii, Digit) => push!(i),
91 (Lower | Upper, Digit) => {} (Digit, Lower | Upper | NonAscii) => {}
93 (_, Dot) => {}
94 (Dot, _) => match (k2, k0) {
95 (None, _) => push!(i),
96 (Some(_), _) => {
97 push!(i - 1);
98 push!(i);
99 }
100 },
101 _ => push!(i),
102 }
103 }
104 }
105 }
106
107 k2 = k1;
108 k1 = Some(k0);
109 i += 1;
110 }
111 push!(i);
112
113 Self { buf, len: pos }
114 }
115
116 const fn words_count(&self) -> usize {
117 self.len - 1
118 }
119
120 const fn word_range(&self, idx: usize) -> Range<usize> {
121 self.buf[idx]..self.buf[idx + 1]
122 }
123}
124
125pub enum AsciiCase {
126 Lower,
127 Upper,
128 LowerCamel,
129 UpperCamel,
130 Title,
131 Snake,
132 Kebab,
133 ShoutySnake,
134 ShoutyKebab,
135}
136
137impl AsciiCase {
138 const fn get_seperator(&self) -> Option<u8> {
139 match self {
140 Self::Title => Some(b' '),
141 Self::Snake | Self::ShoutySnake => Some(b'_'),
142 Self::Kebab | Self::ShoutyKebab => Some(b'-'),
143 _ => None,
144 }
145 }
146}
147
148pub struct ConvAsciiCase<T>(pub T, pub AsciiCase);
149
150impl ConvAsciiCase<&str> {
151 pub const fn output_len<const M: usize>(&self) -> usize {
152 assert!(self.0.len() + 1 == M);
153
154 use AsciiCase::*;
155 match self.1 {
156 Lower | Upper => self.0.len(),
157 LowerCamel | UpperCamel | Title | Snake | Kebab | ShoutySnake | ShoutyKebab => {
158 let mut ans = 0;
159
160 let has_sep = self.1.get_seperator().is_some();
161
162 let boundaries = Boundaries::<M>::new(self.0);
163 let words_count = boundaries.words_count();
164
165 let mut i = 0;
166 let mut is_starting_boundary: bool = true;
167
168 while i < words_count {
169 let rng = boundaries.word_range(i);
170 let word = subslice(self.0.as_bytes(), rng);
171
172 if !TokenKind::is_boundary_word(word) {
173 if has_sep && !is_starting_boundary {
174 ans += 1;
175 }
176 ans += word.len();
177 is_starting_boundary = false;
178 }
179
180 i += 1;
181 }
182 ans
183 }
184 }
185 }
186
187 pub const fn const_eval<const M: usize, const N: usize>(&self) -> StrBuf<N> {
188 assert!(self.0.len() + 1 == M);
189
190 let mut buf = [0; N];
191 let mut pos = 0;
192 let s = self.0.as_bytes();
193
194 macro_rules! push {
195 ($x: expr) => {{
196 buf[pos] = $x;
197 pos += 1;
198 }};
199 }
200
201 use AsciiCase::*;
202 match self.1 {
203 Lower => {
204 while pos < s.len() {
205 push!(s[pos].to_ascii_lowercase());
206 }
207 }
208 Upper => {
209 while pos < s.len() {
210 push!(s[pos].to_ascii_uppercase());
211 }
212 }
213 LowerCamel | UpperCamel | Title | Snake | Kebab | ShoutySnake | ShoutyKebab => {
214 let sep = self.1.get_seperator();
215
216 let boundaries = Boundaries::<M>::new(self.0);
217 let words_count = boundaries.words_count();
218
219 let mut i = 0;
220 let mut is_starting_boundary = true;
221
222 while i < words_count {
223 let rng = boundaries.word_range(i);
224 let word = subslice(self.0.as_bytes(), rng);
225
226 if !TokenKind::is_boundary_word(word) {
227 if let (Some(sep), false) = (sep, is_starting_boundary) {
228 push!(sep)
229 }
230 let mut j = 0;
231 while j < word.len() {
232 let b = match self.1 {
233 Snake | Kebab => word[j].to_ascii_lowercase(),
234 ShoutySnake | ShoutyKebab => word[j].to_ascii_uppercase(),
235 LowerCamel | UpperCamel | Title => {
236 let is_upper = match self.1 {
237 LowerCamel => !is_starting_boundary && j == 0,
238 UpperCamel | Title => j == 0,
239 _ => unreachable!(),
240 };
241 if is_upper {
242 word[j].to_ascii_uppercase()
243 } else {
244 word[j].to_ascii_lowercase()
245 }
246 }
247 _ => unreachable!(),
248 };
249 push!(b);
250 j += 1;
251 }
252 is_starting_boundary = false;
253 }
254
255 i += 1;
256 }
257 }
258 }
259
260 assert!(pos == N);
261
262 unsafe { StrBuf::new_unchecked(buf) }
263 }
264}
265
266#[doc(hidden)]
267#[macro_export]
268macro_rules! __conv_ascii_case {
269 ($s: expr, $case: expr) => {{
270 const INPUT: &str = $s;
271 const M: usize = INPUT.len() + 1;
272 const N: usize = $crate::__ctfe::ConvAsciiCase(INPUT, $case).output_len::<M>();
273 const OUTPUT_BUF: $crate::__ctfe::StrBuf<N> =
274 $crate::__ctfe::ConvAsciiCase(INPUT, $case).const_eval::<M, N>();
275 OUTPUT_BUF.as_str()
276 }};
277}
278
279#[macro_export]
309macro_rules! convert_ascii_case {
310 (lower, $s: expr) => {
311 $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::Lower)
312 };
313 (upper, $s: expr) => {
314 $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::Upper)
315 };
316 (lower_camel, $s: expr) => {
317 $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::LowerCamel)
318 };
319 (upper_camel, $s: expr) => {
320 $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::UpperCamel)
321 };
322 (title, $s: expr) => {
323 $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::Title)
324 };
325 (snake, $s: expr) => {
326 $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::Snake)
327 };
328 (kebab, $s: expr) => {
329 $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::Kebab)
330 };
331 (shouty_snake, $s: expr) => {
332 $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::ShoutySnake)
333 };
334 (shouty_kebab, $s: expr) => {
335 $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::ShoutyKebab)
336 };
337}
338
339#[cfg(test)]
340mod tests {
341 #[test]
342 fn test_conv_ascii_case() {
343 macro_rules! test_conv_ascii_case {
344 ($v: tt, $a: expr, $b: expr $(,)?) => {{
345 const A: &str = $a;
346 const B: &str = convert_ascii_case!($v, A);
347 assert_eq!(B, $b);
348 test_conv_ascii_case!(heck, $v, $a, $b);
349 }};
350 (heck, assert_eq, $c: expr, $b: expr) => {{
351 if $c != $b {
352 println!("heck mismatch:\nheck: {:?}\nexpected: {:?}\n", $c, $b);
353 }
354 }};
355 (heck, lower_camel, $a: expr, $b: expr) => {{
356 use heck::ToLowerCamelCase;
357 let c: String = $a.to_lower_camel_case();
358 test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
359 }};
360 (heck, upper_camel, $a: expr, $b: expr) => {{
361 use heck::ToUpperCamelCase;
362 let c: String = $a.to_upper_camel_case();
363 test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
364 }};
365 (heck, title, $a: expr, $b: expr) => {{
366 use heck::ToTitleCase;
367 let c: String = $a.to_title_case();
368 test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
369 }};
370 (heck, snake, $a: expr, $b: expr) => {{
371 use heck::ToSnakeCase;
372 let c: String = $a.to_snake_case();
373 test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
374 }};
375 (heck, kebab, $a: expr, $b: expr) => {{
376 use heck::ToKebabCase;
377 let c: String = $a.to_kebab_case();
378 test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
379 }};
380 (heck, shouty_snake, $a: expr, $b: expr) => {{
381 use heck::ToShoutySnakeCase;
382 let c: String = $a.to_shouty_snake_case();
383 test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
384 }};
385 (heck, shouty_kebab, $a: expr, $b: expr) => {{
386 use heck::ToShoutyKebabCase;
387 let c: String = $a.to_shouty_kebab_case();
388 test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
389 }};
390 }
391
392 {
393 const S: &str = "b.8";
394 test_conv_ascii_case!(lower_camel, S, "b8");
395 test_conv_ascii_case!(upper_camel, S, "B8");
396 test_conv_ascii_case!(title, S, "B 8");
397 test_conv_ascii_case!(snake, S, "b_8");
398 test_conv_ascii_case!(kebab, S, "b-8");
399 test_conv_ascii_case!(shouty_snake, S, "B_8");
400 test_conv_ascii_case!(shouty_kebab, S, "B-8");
401 }
402
403 {
404 const S: &str = "Hello World123!XMLHttp我4t5.c6.7b.8";
405 test_conv_ascii_case!(lower_camel, S, "helloWorld123XmlHttp我4t5C67b8");
406 test_conv_ascii_case!(upper_camel, S, "HelloWorld123XmlHttp我4t5C67b8");
407 test_conv_ascii_case!(title, S, "Hello World123 Xml Http 我 4t5 C6 7b 8");
408 test_conv_ascii_case!(snake, S, "hello_world123_xml_http_我_4t5_c6_7b_8");
409 test_conv_ascii_case!(kebab, S, "hello-world123-xml-http-我-4t5-c6-7b-8");
410 test_conv_ascii_case!(shouty_snake, S, "HELLO_WORLD123_XML_HTTP_我_4T5_C6_7B_8");
411 test_conv_ascii_case!(shouty_kebab, S, "HELLO-WORLD123-XML-HTTP-我-4T5-C6-7B-8");
412 }
413 {
414 const S: &str = "XMLHttpRequest";
415 test_conv_ascii_case!(lower_camel, S, "xmlHttpRequest");
416 test_conv_ascii_case!(upper_camel, S, "XmlHttpRequest");
417 test_conv_ascii_case!(title, S, "Xml Http Request");
418 test_conv_ascii_case!(snake, S, "xml_http_request");
419 test_conv_ascii_case!(kebab, S, "xml-http-request");
420 test_conv_ascii_case!(shouty_snake, S, "XML_HTTP_REQUEST");
421 test_conv_ascii_case!(shouty_kebab, S, "XML-HTTP-REQUEST");
422 }
423 {
424 const S: &str = " hello world ";
425 test_conv_ascii_case!(lower_camel, S, "helloWorld");
426 test_conv_ascii_case!(upper_camel, S, "HelloWorld");
427 test_conv_ascii_case!(title, S, "Hello World");
428 test_conv_ascii_case!(snake, S, "hello_world");
429 test_conv_ascii_case!(kebab, S, "hello-world");
430 test_conv_ascii_case!(shouty_snake, S, "HELLO_WORLD");
431 test_conv_ascii_case!(shouty_kebab, S, "HELLO-WORLD");
432 }
433 {
434 const S: &str = "";
435 test_conv_ascii_case!(lower_camel, S, "");
436 test_conv_ascii_case!(upper_camel, S, "");
437 test_conv_ascii_case!(title, S, "");
438 test_conv_ascii_case!(snake, S, "");
439 test_conv_ascii_case!(kebab, S, "");
440 test_conv_ascii_case!(shouty_snake, S, "");
441 test_conv_ascii_case!(shouty_kebab, S, "");
442 }
443 {
444 const S: &str = "_";
445 test_conv_ascii_case!(lower_camel, S, "");
446 test_conv_ascii_case!(upper_camel, S, "");
447 test_conv_ascii_case!(title, S, "");
448 test_conv_ascii_case!(snake, S, "");
449 test_conv_ascii_case!(kebab, S, "");
450 test_conv_ascii_case!(shouty_snake, S, "");
451 test_conv_ascii_case!(shouty_kebab, S, "");
452 }
453 {
454 const S: &str = "1.2E3";
455 test_conv_ascii_case!(lower_camel, S, "12e3");
456 test_conv_ascii_case!(upper_camel, S, "12e3");
457 test_conv_ascii_case!(title, S, "1 2e3");
458 test_conv_ascii_case!(snake, S, "1_2e3");
459 test_conv_ascii_case!(kebab, S, "1-2e3");
460 test_conv_ascii_case!(shouty_snake, S, "1_2E3");
461 test_conv_ascii_case!(shouty_kebab, S, "1-2E3");
462 }
463 {
464 const S: &str = "__a__b-c__d__";
465 test_conv_ascii_case!(lower_camel, S, "aBCD");
466 test_conv_ascii_case!(upper_camel, S, "ABCD");
467 test_conv_ascii_case!(snake, S, "a_b_c_d");
468 test_conv_ascii_case!(kebab, S, "a-b-c-d");
469 test_conv_ascii_case!(shouty_snake, S, "A_B_C_D");
470 test_conv_ascii_case!(shouty_kebab, S, "A-B-C-D");
471 }
472 {
473 const S: &str = "futures-core123";
474 test_conv_ascii_case!(lower_camel, S, "futuresCore123");
475 test_conv_ascii_case!(upper_camel, S, "FuturesCore123");
476 test_conv_ascii_case!(snake, S, "futures_core123");
477 test_conv_ascii_case!(kebab, S, "futures-core123");
478 test_conv_ascii_case!(shouty_snake, S, "FUTURES_CORE123");
479 test_conv_ascii_case!(shouty_kebab, S, "FUTURES-CORE123");
480 }
481 }
482}