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