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) => {} (NonAscii, Lower | Upper) => {} (Lower | Upper, Digit) => {} (Digit, Lower | Upper | NonAscii) => {}
94 (Lower | Upper, NonAscii) => {} (_, Dot) => {}
96 (Dot, _) => match (k2, k0) {
97 (None, _) => push!(i),
98 (Some(_), _) => {
99 push!(i - 1);
100 push!(i);
101 }
102 },
103 _ => push!(i),
104 }
105 }
106 }
107 }
108
109 k2 = k1;
110 k1 = Some(k0);
111 i += 1;
112 }
113 push!(i);
114
115 Self { buf, len: pos }
116 }
117
118 const fn words_count(&self) -> usize {
119 self.len - 1
120 }
121
122 const fn word_range(&self, idx: usize) -> Range<usize> {
123 self.buf[idx]..self.buf[idx + 1]
124 }
125}
126
127pub enum AsciiCase {
128 Lower,
129 Upper,
130 LowerCamel,
131 UpperCamel,
132 Title,
133 Train,
134 Snake,
135 Kebab,
136 ShoutySnake,
137 ShoutyKebab,
138}
139
140impl AsciiCase {
141 const fn get_seperator(&self) -> Option<u8> {
142 match self {
143 Self::Title => Some(b' '),
144 Self::Snake | Self::ShoutySnake => Some(b'_'),
145 Self::Train | Self::Kebab | Self::ShoutyKebab => Some(b'-'),
146 _ => None,
147 }
148 }
149}
150
151pub struct ConvAsciiCase<T>(pub T, pub AsciiCase);
152
153impl ConvAsciiCase<&str> {
154 pub const fn output_len<const M: usize>(&self) -> usize {
155 assert!(self.0.len() + 1 == M);
156
157 use AsciiCase::*;
158 match self.1 {
159 Lower | Upper => self.0.len(),
160 LowerCamel | UpperCamel | Title | Train | Snake | Kebab | ShoutySnake | ShoutyKebab => {
161 let mut ans = 0;
162
163 let has_sep = self.1.get_seperator().is_some();
164
165 let boundaries = Boundaries::<M>::new(self.0);
166 let words_count = boundaries.words_count();
167
168 let mut i = 0;
169 let mut is_starting_boundary: bool = true;
170
171 while i < words_count {
172 let rng = boundaries.word_range(i);
173 let word = subslice(self.0.as_bytes(), rng);
174
175 if !TokenKind::is_boundary_word(word) {
176 if has_sep && !is_starting_boundary {
177 ans += 1;
178 }
179 ans += word.len();
180 is_starting_boundary = false;
181 }
182
183 i += 1;
184 }
185 ans
186 }
187 }
188 }
189
190 pub const fn const_eval<const M: usize, const N: usize>(&self) -> StrBuf<N> {
191 assert!(self.0.len() + 1 == M);
192
193 let mut buf = [0; N];
194 let mut pos = 0;
195 let s = self.0.as_bytes();
196
197 macro_rules! push {
198 ($x: expr) => {{
199 buf[pos] = $x;
200 pos += 1;
201 }};
202 }
203
204 use AsciiCase::*;
205 match self.1 {
206 Lower => {
207 while pos < s.len() {
208 push!(s[pos].to_ascii_lowercase());
209 }
210 }
211 Upper => {
212 while pos < s.len() {
213 push!(s[pos].to_ascii_uppercase());
214 }
215 }
216 LowerCamel | UpperCamel | Title | Train | Snake | Kebab | ShoutySnake | ShoutyKebab => {
217 let sep = self.1.get_seperator();
218
219 let boundaries = Boundaries::<M>::new(self.0);
220 let words_count = boundaries.words_count();
221
222 let mut i = 0;
223 let mut is_starting_boundary = true;
224
225 while i < words_count {
226 let rng = boundaries.word_range(i);
227 let word = subslice(self.0.as_bytes(), rng);
228
229 if !TokenKind::is_boundary_word(word) {
230 if let (Some(sep), false) = (sep, is_starting_boundary) {
231 push!(sep)
232 }
233 let mut j = 0;
234 while j < word.len() {
235 let b = match self.1 {
236 Snake | Kebab => word[j].to_ascii_lowercase(),
237 ShoutySnake | ShoutyKebab => word[j].to_ascii_uppercase(),
238 LowerCamel | UpperCamel | Title | Train => {
239 let is_upper = match self.1 {
240 LowerCamel => !is_starting_boundary && j == 0,
241 UpperCamel | Title | Train => j == 0,
242 _ => unreachable!(),
243 };
244 if is_upper {
245 word[j].to_ascii_uppercase()
246 } else {
247 word[j].to_ascii_lowercase()
248 }
249 }
250 _ => unreachable!(),
251 };
252 push!(b);
253 j += 1;
254 }
255 is_starting_boundary = false;
256 }
257
258 i += 1;
259 }
260 }
261 }
262
263 assert!(pos == N);
264
265 unsafe { StrBuf::new_unchecked(buf) }
266 }
267}
268
269#[doc(hidden)]
270#[macro_export]
271macro_rules! __conv_ascii_case {
272 ($s: expr, $case: expr) => {{
273 const INPUT: &str = $s;
274 const M: usize = INPUT.len() + 1;
275 const N: usize = $crate::__ctfe::ConvAsciiCase(INPUT, $case).output_len::<M>();
276 const OUTPUT_BUF: $crate::__ctfe::StrBuf<N> =
277 $crate::__ctfe::ConvAsciiCase(INPUT, $case).const_eval::<M, N>();
278 OUTPUT_BUF.as_str()
279 }};
280}
281
282#[macro_export]
314macro_rules! convert_ascii_case {
315 (lower, $s: expr) => {
316 $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::Lower)
317 };
318 (upper, $s: expr) => {
319 $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::Upper)
320 };
321 (lower_camel, $s: expr) => {
322 $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::LowerCamel)
323 };
324 (upper_camel, $s: expr) => {
325 $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::UpperCamel)
326 };
327 (title, $s: expr) => {
328 $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::Title)
329 };
330 (train, $s: expr) => {
331 $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::Train)
332 };
333 (snake, $s: expr) => {
334 $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::Snake)
335 };
336 (kebab, $s: expr) => {
337 $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::Kebab)
338 };
339 (shouty_snake, $s: expr) => {
340 $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::ShoutySnake)
341 };
342 (shouty_kebab, $s: expr) => {
343 $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::ShoutyKebab)
344 };
345}
346
347#[cfg(test)]
348mod tests {
349 #[test]
350 fn test_conv_ascii_case() {
351 macro_rules! test_conv_ascii_case {
352 ($v: tt, $a: expr, $b: expr $(,)?) => {{
353 const A: &str = $a;
354 const B: &str = convert_ascii_case!($v, A);
355 assert_eq!(B, $b);
356 test_conv_ascii_case!(heck, $v, $a, $b);
357 }};
358 (heck, assert_eq, $c: expr, $b: expr) => {{
359 if $c != $b {
360 println!("heck mismatch:\nheck: {:?}\nexpected: {:?}\n", $c, $b);
361 }
362 }};
363 (heck, lower_camel, $a: expr, $b: expr) => {{
364 use heck::ToLowerCamelCase;
365 let c: String = $a.to_lower_camel_case();
366 test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
367 }};
368 (heck, upper_camel, $a: expr, $b: expr) => {{
369 use heck::ToUpperCamelCase;
370 let c: String = $a.to_upper_camel_case();
371 test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
372 }};
373 (heck, title, $a: expr, $b: expr) => {{
374 use heck::ToTitleCase;
375 let c: String = $a.to_title_case();
376 test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
377 }};
378 (heck, train, $a: expr, $b: expr) => {{
379 use heck::ToTrainCase;
380 let c: String = $a.to_train_case();
381 test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
382 }};
383 (heck, snake, $a: expr, $b: expr) => {{
384 use heck::ToSnakeCase;
385 let c: String = $a.to_snake_case();
386 test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
387 }};
388 (heck, kebab, $a: expr, $b: expr) => {{
389 use heck::ToKebabCase;
390 let c: String = $a.to_kebab_case();
391 test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
392 }};
393 (heck, shouty_snake, $a: expr, $b: expr) => {{
394 use heck::ToShoutySnakeCase;
395 let c: String = $a.to_shouty_snake_case();
396 test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
397 }};
398 (heck, shouty_kebab, $a: expr, $b: expr) => {{
399 use heck::ToShoutyKebabCase;
400 let c: String = $a.to_shouty_kebab_case();
401 test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
402 }};
403 }
404
405 {
406 const S: &str = "b.8";
407 test_conv_ascii_case!(lower_camel, S, "b8");
408 test_conv_ascii_case!(upper_camel, S, "B8");
409 test_conv_ascii_case!(title, S, "B 8");
410 test_conv_ascii_case!(train, S, "B-8");
411 test_conv_ascii_case!(snake, S, "b_8");
412 test_conv_ascii_case!(kebab, S, "b-8");
413 test_conv_ascii_case!(shouty_snake, S, "B_8");
414 test_conv_ascii_case!(shouty_kebab, S, "B-8");
415 }
416
417 {
418 const S: &str = "Hello World123!XMLHttp我4t5.c6.7b.8";
419 test_conv_ascii_case!(lower_camel, S, "helloWorld123XmlHttp我4t5C67b8");
420 test_conv_ascii_case!(upper_camel, S, "HelloWorld123XmlHttp我4t5C67b8");
421 test_conv_ascii_case!(title, S, "Hello World123 Xml Http我4t5 C6 7b 8");
422 test_conv_ascii_case!(train, S, "Hello-World123-Xml-Http我4t5-C6-7b-8");
423 test_conv_ascii_case!(snake, S, "hello_world123_xml_http我4t5_c6_7b_8");
424 test_conv_ascii_case!(kebab, S, "hello-world123-xml-http我4t5-c6-7b-8");
425 test_conv_ascii_case!(shouty_snake, S, "HELLO_WORLD123_XML_HTTP我4T5_C6_7B_8");
426 test_conv_ascii_case!(shouty_kebab, S, "HELLO-WORLD123-XML-HTTP我4T5-C6-7B-8");
427 }
428 {
429 const S: &str = "XMLHttpRequest";
430 test_conv_ascii_case!(lower_camel, S, "xmlHttpRequest");
431 test_conv_ascii_case!(upper_camel, S, "XmlHttpRequest");
432 test_conv_ascii_case!(title, S, "Xml Http Request");
433 test_conv_ascii_case!(train, S, "Xml-Http-Request");
434 test_conv_ascii_case!(snake, S, "xml_http_request");
435 test_conv_ascii_case!(kebab, S, "xml-http-request");
436 test_conv_ascii_case!(shouty_snake, S, "XML_HTTP_REQUEST");
437 test_conv_ascii_case!(shouty_kebab, S, "XML-HTTP-REQUEST");
438 }
439 {
440 const S: &str = " hello world ";
441 test_conv_ascii_case!(lower_camel, S, "helloWorld");
442 test_conv_ascii_case!(upper_camel, S, "HelloWorld");
443 test_conv_ascii_case!(title, S, "Hello World");
444 test_conv_ascii_case!(train, S, "Hello-World");
445 test_conv_ascii_case!(snake, S, "hello_world");
446 test_conv_ascii_case!(kebab, S, "hello-world");
447 test_conv_ascii_case!(shouty_snake, S, "HELLO_WORLD");
448 test_conv_ascii_case!(shouty_kebab, S, "HELLO-WORLD");
449 }
450 {
451 const S: &str = "";
452 test_conv_ascii_case!(lower_camel, S, "");
453 test_conv_ascii_case!(upper_camel, S, "");
454 test_conv_ascii_case!(title, S, "");
455 test_conv_ascii_case!(train, S, "");
456 test_conv_ascii_case!(snake, S, "");
457 test_conv_ascii_case!(kebab, S, "");
458 test_conv_ascii_case!(shouty_snake, S, "");
459 test_conv_ascii_case!(shouty_kebab, S, "");
460 }
461 {
462 const S: &str = "_";
463 test_conv_ascii_case!(lower_camel, S, "");
464 test_conv_ascii_case!(upper_camel, S, "");
465 test_conv_ascii_case!(title, S, "");
466 test_conv_ascii_case!(train, S, "");
467 test_conv_ascii_case!(snake, S, "");
468 test_conv_ascii_case!(kebab, S, "");
469 test_conv_ascii_case!(shouty_snake, S, "");
470 test_conv_ascii_case!(shouty_kebab, S, "");
471 }
472 {
473 const S: &str = "1.2E3";
474 test_conv_ascii_case!(lower_camel, S, "12e3");
475 test_conv_ascii_case!(upper_camel, S, "12e3");
476 test_conv_ascii_case!(title, S, "1 2e3");
477 test_conv_ascii_case!(train, S, "1-2e3");
478 test_conv_ascii_case!(snake, S, "1_2e3");
479 test_conv_ascii_case!(kebab, S, "1-2e3");
480 test_conv_ascii_case!(shouty_snake, S, "1_2E3");
481 test_conv_ascii_case!(shouty_kebab, S, "1-2E3");
482 }
483 {
484 const S: &str = "__a__b-c__d__";
485 test_conv_ascii_case!(lower_camel, S, "aBCD");
486 test_conv_ascii_case!(upper_camel, S, "ABCD");
487 test_conv_ascii_case!(title, S, "A B C D");
488 test_conv_ascii_case!(train, S, "A-B-C-D");
489 test_conv_ascii_case!(snake, S, "a_b_c_d");
490 test_conv_ascii_case!(kebab, S, "a-b-c-d");
491 test_conv_ascii_case!(shouty_snake, S, "A_B_C_D");
492 test_conv_ascii_case!(shouty_kebab, S, "A-B-C-D");
493 }
494 {
495 const S: &str = "futures-core123";
496 test_conv_ascii_case!(lower_camel, S, "futuresCore123");
497 test_conv_ascii_case!(upper_camel, S, "FuturesCore123");
498 test_conv_ascii_case!(title, S, "Futures Core123");
499 test_conv_ascii_case!(train, S, "Futures-Core123");
500 test_conv_ascii_case!(snake, S, "futures_core123");
501 test_conv_ascii_case!(kebab, S, "futures-core123");
502 test_conv_ascii_case!(shouty_snake, S, "FUTURES_CORE123");
503 test_conv_ascii_case!(shouty_kebab, S, "FUTURES-CORE123");
504 }
505 }
506}