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