1use base64::{Engine as _, engine::general_purpose};
2use chrono::{DateTime, Utc};
3use pulldown_cmark::{Options, Parser, html};
4use rand::{Rng, thread_rng};
5use regex::Regex;
6use std::collections::HashMap;
7use std::sync::LazyLock; use std::sync::{Arc, Mutex, OnceLock};
9
10use ulid::Ulid;
11use unicode_segmentation::UnicodeSegmentation;
12use unidecode::unidecode;
13use uuid::Uuid;
14
15type UuidSequence = Arc<Mutex<Option<(Vec<Uuid>, usize)>>>;
17type UlidSequence = Arc<Mutex<Option<(Vec<Ulid>, usize)>>>;
18type RandomSequence = Arc<Mutex<Option<(Vec<String>, usize)>>>;
19
20static SNAKE_CACHE: LazyLock<Arc<Mutex<HashMap<String, String>>>> =
22 LazyLock::new(|| Arc::new(Mutex::new(HashMap::new())));
23static CAMEL_CACHE: LazyLock<Arc<Mutex<HashMap<String, String>>>> =
24 LazyLock::new(|| Arc::new(Mutex::new(HashMap::new())));
25static STUDLY_CACHE: LazyLock<Arc<Mutex<HashMap<String, String>>>> =
26 LazyLock::new(|| Arc::new(Mutex::new(HashMap::new())));
27
28static UUID_FACTORY: OnceLock<Box<dyn Fn() -> Uuid + Send + Sync>> = OnceLock::new();
30static ULID_FACTORY: OnceLock<Box<dyn Fn() -> Ulid + Send + Sync>> = OnceLock::new();
31static RANDOM_STRING_FACTORY: OnceLock<Box<dyn Fn(usize) -> String + Send + Sync>> =
32 OnceLock::new();
33
34static UUID_SEQUENCE: LazyLock<UuidSequence> = LazyLock::new(|| Arc::new(Mutex::new(None)));
36static ULID_SEQUENCE: LazyLock<UlidSequence> = LazyLock::new(|| Arc::new(Mutex::new(None)));
37static RANDOM_SEQUENCE: LazyLock<RandomSequence> = LazyLock::new(|| Arc::new(Mutex::new(None)));
38
39const INVISIBLE_CHARACTERS: &str = "\u{0009}\u{0020}\u{00A0}\u{00AD}\u{034F}\u{061C}\u{115F}\u{1160}\u{17B4}\u{17B5}\u{180E}\u{2000}\u{2001}\u{2002}\u{2003}\u{2004}\u{2005}\u{2006}\u{2007}\u{2008}\u{2009}\u{200A}\u{200B}\u{200C}\u{200D}\u{200E}\u{200F}\u{202F}\u{205F}\u{2060}\u{2061}\u{2062}\u{2063}\u{2064}\u{2065}\u{206A}\u{206B}\u{206C}\u{206D}\u{206E}\u{206F}\u{3000}\u{2800}\u{3164}\u{FEFF}\u{FFA0}\u{1D159}\u{1D173}\u{1D174}\u{1D175}\u{1D176}\u{1D177}\u{1D178}\u{1D179}\u{1D17A}\u{E0020}";
41
42static PLURAL_RULES: LazyLock<Vec<(Regex, &'static str)>> = LazyLock::new(|| {
44 vec![
45 (Regex::new(r"(?i)^(child)$").unwrap(), "${1}ren"),
46 (Regex::new(r"(?i)(quiz)$").unwrap(), "${1}zes"),
47 (Regex::new(r"(?i)^(ox)$").unwrap(), "${1}en"),
48 (Regex::new(r"(?i)([m|l])ouse$").unwrap(), "${1}ice"),
49 (
50 Regex::new(r"(?i)(matr|vert|ind)ix|ex$").unwrap(),
51 "${1}ices",
52 ),
53 (Regex::new(r"(?i)(x|ch|ss|sh)$").unwrap(), "${1}es"),
54 (Regex::new(r"(?i)([^aeiouy]|qu)y$").unwrap(), "${1}ies"),
55 (Regex::new(r"(?i)(hive)$").unwrap(), "${1}s"),
56 (
57 Regex::new(r"(?i)(?:([^f])fe|([lr])f)$").unwrap(),
58 "${1}${2}ves",
59 ),
60 (Regex::new(r"(?i)(shea|lea|loa|thie)f$").unwrap(), "${1}ves"),
61 (Regex::new(r"(?i)sis$").unwrap(), "ses"),
62 (Regex::new(r"(?i)([ti])um$").unwrap(), "${1}a"),
63 (
64 Regex::new(r"(?i)(tomat|potat|ech|her|vet)o$").unwrap(),
65 "${1}oes",
66 ),
67 (Regex::new(r"(?i)(bu)s$").unwrap(), "${1}ses"),
68 (Regex::new(r"(?i)(alias)$").unwrap(), "${1}es"),
69 (Regex::new(r"(?i)(octop)us$").unwrap(), "${1}i"),
70 (Regex::new(r"(?i)(ax|test)is$").unwrap(), "${1}es"),
71 (Regex::new(r"(?i)(us)$").unwrap(), "${1}es"),
72 (Regex::new(r"(?i)s$").unwrap(), "s"),
73 (Regex::new(r"(?i)$").unwrap(), "s"),
74 ]
75});
76
77static SINGULAR_RULES: LazyLock<Vec<(Regex, &'static str)>> = LazyLock::new(|| {
78 vec![
79 (Regex::new(r"(?i)^(child)ren$").unwrap(), "${1}"),
80 (Regex::new(r"(?i)(quiz)zes$").unwrap(), "${1}"),
81 (Regex::new(r"(?i)(matr)ices$").unwrap(), "${1}ix"),
82 (Regex::new(r"(?i)(vert|ind)ices$").unwrap(), "${1}ex"),
83 (Regex::new(r"(?i)^(ox)en").unwrap(), "${1}"),
84 (Regex::new(r"(?i)(alias)es$").unwrap(), "${1}"),
85 (Regex::new(r"(?i)(octop|vir)i$").unwrap(), "${1}us"),
86 (Regex::new(r"(?i)(cris|ax|test)es$").unwrap(), "${1}is"),
87 (Regex::new(r"(?i)(shoe)s$").unwrap(), "${1}"),
88 (Regex::new(r"(?i)(o)es$").unwrap(), "${1}"),
89 (Regex::new(r"(?i)(bus)es$").unwrap(), "${1}"),
90 (Regex::new(r"(?i)([m|l])ice$").unwrap(), "${1}ouse"),
91 (Regex::new(r"(?i)(x|ch|ss|sh)es$").unwrap(), "${1}"),
92 (Regex::new(r"(?i)(m)ovies$").unwrap(), "${1}ovie"),
93 (Regex::new(r"(?i)(s)eries$").unwrap(), "${1}eries"),
94 (Regex::new(r"(?i)([^aeiouy]|qu)ies$").unwrap(), "${1}y"),
95 (Regex::new(r"(?i)([lr])ves$").unwrap(), "${1}f"),
96 (Regex::new(r"(?i)(tive)s$").unwrap(), "${1}"),
97 (Regex::new(r"(?i)(hive)s$").unwrap(), "${1}"),
98 (Regex::new(r"(?i)(li|wi|kni)ves$").unwrap(), "${1}fe"),
99 (Regex::new(r"(?i)(shea|loa|lea|thie)ves$").unwrap(), "${1}f"),
100 (Regex::new(r"(?i)(^analy)ses$").unwrap(), "${1}sis"),
101 (
102 Regex::new(r"(?i)((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$")
103 .unwrap(),
104 "${1}${2}sis",
105 ),
106 (Regex::new(r"(?i)([ti])a$").unwrap(), "${1}um"),
107 (Regex::new(r"(?i)(n)ews$").unwrap(), "${1}ews"),
108 (Regex::new(r"(?i)(h|bl)ouses$").unwrap(), "${1}ouse"),
109 (Regex::new(r"(?i)(corpse)s$").unwrap(), "${1}"),
110 (Regex::new(r"(?i)(us)es$").unwrap(), "${1}"),
111 (Regex::new(r"(?i)s$").unwrap(), ""),
112 ]
113});
114
115pub struct Str;
116
117impl Str {
118 pub fn after(subject: &str, search: &str) -> String {
120 if search.is_empty() {
121 return subject.to_string();
122 }
123
124 if let Some(pos) = subject.find(search) {
125 subject[(pos + search.len())..].to_string()
126 } else {
127 subject.to_string()
128 }
129 }
130
131 pub fn after_last(subject: &str, search: &str) -> String {
133 if search.is_empty() {
134 return subject.to_string();
135 }
136
137 if let Some(pos) = subject.rfind(search) {
138 subject[(pos + search.len())..].to_string()
139 } else {
140 subject.to_string()
141 }
142 }
143
144 pub fn ascii(value: &str) -> String {
146 unidecode(value)
147 }
148
149 pub fn transliterate(string: &str, unknown: Option<&str>, strict: Option<bool>) -> String {
151 let unknown = unknown.unwrap_or("?");
152 let strict = strict.unwrap_or(false);
153
154 string
155 .chars()
156 .map(|c| {
157 if c.is_ascii() {
158 c.to_string()
159 } else {
160 let transliterated = unidecode(&c.to_string());
161 if transliterated.is_empty()
162 || (strict && !transliterated.chars().all(|ch| ch.is_ascii_alphanumeric()))
163 {
164 unknown.to_string()
165 } else {
166 transliterated
167 }
168 }
169 })
170 .collect()
171 }
172
173 pub fn before(subject: &str, search: &str) -> String {
175 if search.is_empty() {
176 return subject.to_string();
177 }
178
179 if let Some(pos) = subject.find(search) {
180 subject[..pos].to_string()
181 } else {
182 subject.to_string()
183 }
184 }
185
186 pub fn before_last(subject: &str, search: &str) -> String {
188 if search.is_empty() {
189 return subject.to_string();
190 }
191
192 if let Some(pos) = subject.rfind(search) {
193 subject[..pos].to_string()
194 } else {
195 subject.to_string()
196 }
197 }
198
199 pub fn between(subject: &str, from: &str, to: &str) -> String {
201 if from.is_empty() || to.is_empty() {
202 return subject.to_string();
203 }
204
205 let after_from = Self::after(subject, from);
206 Self::before_last(&after_from, to)
207 }
208
209 pub fn between_first(subject: &str, from: &str, to: &str) -> String {
211 if from.is_empty() || to.is_empty() {
212 return subject.to_string();
213 }
214
215 let after_from = Self::after(subject, from);
216 Self::before(&after_from, to)
217 }
218
219 pub fn camel(value: &str) -> String {
221 let cache = CAMEL_CACHE.lock().unwrap();
222 if let Some(cached) = cache.get(value) {
223 return cached.clone();
224 }
225 drop(cache);
226
227 let studly = Self::studly(value);
228 let result = Self::lcfirst(&studly);
229
230 let mut cache = CAMEL_CACHE.lock().unwrap();
231 cache.insert(value.to_string(), result.clone());
232 result
233 }
234
235 pub fn char_at(subject: &str, index: isize) -> Option<char> {
237 let chars: Vec<char> = subject.chars().collect();
238 let length = chars.len() as isize;
239
240 let actual_index = if index < 0 {
241 if index < -length {
242 return None;
243 }
244 (length + index) as usize
245 } else {
246 if index >= length {
247 return None;
248 }
249 index as usize
250 };
251
252 chars.get(actual_index).copied()
253 }
254
255 pub fn chop_start(subject: &str, needles: &[&str]) -> String {
257 for needle in needles {
258 if let Some(stripped) = subject.strip_prefix(needle) {
259 return stripped.to_string();
260 }
261 }
262 subject.to_string()
263 }
264
265 pub fn chop_end(subject: &str, needles: &[&str]) -> String {
267 for needle in needles {
268 if let Some(stripped) = subject.strip_suffix(needle) {
269 return stripped.to_string();
270 }
271 }
272 subject.to_string()
273 }
274
275 pub fn contains(haystack: &str, needles: &[&str], ignore_case: bool) -> bool {
277 let haystack_check = if ignore_case {
278 haystack.to_lowercase()
279 } else {
280 haystack.to_string()
281 };
282
283 for needle in needles {
284 if needle.is_empty() {
285 continue;
286 }
287
288 let needle_check = if ignore_case {
289 needle.to_lowercase()
290 } else {
291 needle.to_string()
292 };
293
294 if haystack_check.contains(&needle_check) {
295 return true;
296 }
297 }
298 false
299 }
300
301 pub fn contains_all(haystack: &str, needles: &[&str], ignore_case: bool) -> bool {
303 for needle in needles {
304 if !Self::contains(haystack, &[needle], ignore_case) {
305 return false;
306 }
307 }
308 true
309 }
310
311 pub fn doesnt_contain(haystack: &str, needles: &[&str], ignore_case: bool) -> bool {
313 !Self::contains(haystack, needles, ignore_case)
314 }
315
316 pub fn convert_case(string: &str, mode: CaseMode) -> String {
318 match mode {
319 CaseMode::Upper => string.to_uppercase(),
320 CaseMode::Lower => string.to_lowercase(),
321 CaseMode::Title => Self::title(string),
322 CaseMode::Fold => string.to_lowercase(), }
324 }
325
326 pub fn deduplicate(string: &str, characters: &[char]) -> String {
328 let mut result = string.to_string();
329
330 for &character in characters {
331 let pattern = format!("{}{{2,}}", regex::escape(&character.to_string()));
332 let re = Regex::new(&pattern).unwrap();
333 result = re.replace_all(&result, &character.to_string()).to_string();
334 }
335
336 result
337 }
338
339 pub fn ends_with(haystack: &str, needles: &[&str]) -> bool {
341 for needle in needles {
342 if !needle.is_empty() && haystack.ends_with(needle) {
343 return true;
344 }
345 }
346 false
347 }
348
349 pub fn doesnt_end_with(haystack: &str, needles: &[&str]) -> bool {
351 !Self::ends_with(haystack, needles)
352 }
353
354 pub fn excerpt(text: &str, phrase: &str, options: ExcerptOptions) -> Option<String> {
356 if phrase.is_empty() {
357 return None;
358 }
359
360 let re = Regex::new(&format!(r"(?i)^(.*?)({})(.*)$", regex::escape(phrase))).ok()?;
361 let caps = re.captures(text)?;
362
363 let before = caps.get(1)?.as_str().trim_start();
364 let matched = caps.get(2)?.as_str();
365 let after = caps.get(3)?.as_str().trim_end();
366
367 let start_chars: Vec<char> = before.chars().collect();
368 let start_len = start_chars.len();
369 let start_excerpt = if start_len > options.radius {
370 let excerpt_start = start_len - options.radius;
371 let excerpt: String = start_chars.iter().skip(excerpt_start).collect();
372 format!("{}{}", options.omission, excerpt.trim_start())
373 } else {
374 before.to_string()
375 };
376
377 let end_chars: Vec<char> = after.chars().collect();
378 let end_excerpt = if end_chars.len() > options.radius {
379 let excerpt: String = end_chars.iter().take(options.radius).collect();
380 format!("{}{}", excerpt.trim_end(), options.omission)
381 } else {
382 after.to_string()
383 };
384
385 Some(format!("{}{}{}", start_excerpt, matched, end_excerpt))
386 }
387
388 pub fn finish(value: &str, cap: &str) -> String {
390 let pattern = format!("({})+$", regex::escape(cap));
391 let re = Regex::new(&pattern).unwrap();
392 let trimmed = re.replace_all(value, "");
393 format!("{}{}", trimmed, cap)
394 }
395
396 pub fn wrap(value: &str, before: &str, after: Option<&str>) -> String {
398 let after = after.unwrap_or(before);
399 format!("{}{}{}", before, value, after)
400 }
401
402 pub fn unwrap(value: &str, before: &str, after: Option<&str>) -> String {
404 let after = after.unwrap_or(before);
405 let mut result = value.to_string();
406
407 if result.starts_with(before) {
408 result = result[before.len()..].to_string();
409 }
410
411 if result.ends_with(after) {
412 result = result[..result.len() - after.len()].to_string();
413 }
414
415 result
416 }
417
418 pub fn is_pattern(patterns: &[&str], value: &str, ignore_case: bool) -> bool {
420 for pattern in patterns {
421 if *pattern == "*" || *pattern == value {
422 return true;
423 }
424
425 if ignore_case && pattern.to_lowercase() == value.to_lowercase() {
426 return true;
427 }
428
429 let escaped = regex::escape(pattern);
430 let pattern_regex = escaped.replace("\\*", ".*");
431 let flags = if ignore_case { "(?i)" } else { "" };
432 let full_pattern = format!("{}^{}$", flags, pattern_regex);
433
434 if let Ok(re) = Regex::new(&full_pattern)
435 && re.is_match(value)
436 {
437 return true;
438 }
439 }
440 false
441 }
442
443 pub fn is_ascii(value: &str) -> bool {
445 value.is_ascii()
446 }
447
448 pub fn is_json(value: &str) -> bool {
450 serde_json::from_str::<serde_json::Value>(value).is_ok()
451 }
452
453 pub fn is_url(value: &str, protocols: Option<&[&str]>) -> bool {
455 if let Ok(parsed) = url::Url::parse(value) {
456 if let Some(allowed_protocols) = protocols {
457 allowed_protocols.contains(&parsed.scheme())
458 } else {
459 true
460 }
461 } else {
462 false
463 }
464 }
465
466 pub fn is_uuid(value: &str, version: Option<UuidVersion>) -> bool {
468 if let Ok(uuid) = Uuid::parse_str(value) {
469 match version {
470 Some(UuidVersion::Nil) => uuid.is_nil(),
471 Some(UuidVersion::V1) => uuid.get_version() == Some(uuid::Version::Mac),
472 Some(UuidVersion::V3) => uuid.get_version() == Some(uuid::Version::Md5),
473 Some(UuidVersion::V4) => uuid.get_version() == Some(uuid::Version::Random),
474 Some(UuidVersion::V5) => uuid.get_version() == Some(uuid::Version::Sha1),
475 Some(UuidVersion::Max) => uuid.is_max(),
476 None => true,
477 }
478 } else {
479 false
480 }
481 }
482
483 pub fn is_ulid(value: &str) -> bool {
485 Ulid::from_string(value).is_ok()
486 }
487
488 pub fn kebab(value: &str) -> String {
490 Self::snake(value, "-")
491 }
492
493 pub fn length(value: &str) -> usize {
495 value.graphemes(true).count()
496 }
497
498 pub fn limit(value: &str, limit: usize, end: &str, preserve_words: bool) -> String {
500 let graphemes: Vec<&str> = value.graphemes(true).collect();
501
502 if graphemes.len() <= limit {
503 return value.to_string();
504 }
505
506 if !preserve_words {
507 let truncated: String = graphemes.iter().take(limit).copied().collect();
508 return format!("{}{}", truncated, end);
509 }
510
511 let truncated: String = graphemes.iter().take(limit).copied().collect();
513 let trimmed = truncated.trim_end();
514
515 if let Some(last_space) = trimmed.rfind(' ') {
517 format!("{}{}", &trimmed[..last_space], end)
518 } else {
519 format!("{}{}", trimmed, end)
520 }
521 }
522
523 pub fn lower(value: &str) -> String {
525 value.to_lowercase()
526 }
527
528 pub fn words(value: &str, words: usize, end: &str) -> String {
530 let word_vec: Vec<&str> = value.split_whitespace().collect();
531
532 if word_vec.len() <= words {
533 return value.to_string();
534 }
535
536 let truncated: Vec<&str> = word_vec.iter().take(words).copied().collect();
537 format!("{}{}", truncated.join(" "), end)
538 }
539
540 pub fn markdown(string: &str, options: MarkdownOptions) -> String {
542 let mut md_options = Options::empty();
543 if options.tables {
544 md_options.insert(Options::ENABLE_TABLES);
545 }
546 if options.footnotes {
547 md_options.insert(Options::ENABLE_FOOTNOTES);
548 }
549 if options.strikethrough {
550 md_options.insert(Options::ENABLE_STRIKETHROUGH);
551 }
552 if options.tasklists {
553 md_options.insert(Options::ENABLE_TASKLISTS);
554 }
555
556 let parser = Parser::new_ext(string, md_options);
557 let mut html_output = String::new();
558 html::push_html(&mut html_output, parser);
559 html_output
560 }
561
562 pub fn inline_markdown(string: &str, options: MarkdownOptions) -> String {
564 let html = Self::markdown(string, options);
566 let re = Regex::new(r"</?(?:p|div|h[1-6]|blockquote|pre|ul|ol|li)[^>]*>").unwrap();
567 re.replace_all(&html, "").to_string()
568 }
569
570 pub fn mask(string: &str, character: char, index: isize, length: Option<usize>) -> String {
572 if character == '\0' {
573 return string.to_string();
574 }
575
576 let chars: Vec<char> = string.chars().collect();
577 let str_len = chars.len() as isize;
578
579 let start_index = if index < 0 {
580 if index < -str_len {
581 0
582 } else {
583 (str_len + index) as usize
584 }
585 } else {
586 if index >= str_len {
587 return string.to_string();
588 }
589 index as usize
590 };
591
592 let mask_length = length.unwrap_or(chars.len() - start_index);
593 let end_index = std::cmp::min(start_index + mask_length, chars.len());
594
595 let start: String = chars.iter().take(start_index).collect();
596 let mask: String = character.to_string().repeat(end_index - start_index);
597 let end: String = chars.iter().skip(end_index).collect();
598
599 format!("{}{}{}", start, mask, end)
600 }
601
602 pub fn match_pattern(pattern: &str, subject: &str) -> String {
604 if let Ok(re) = Regex::new(pattern) {
605 if let Some(caps) = re.captures(subject) {
606 if let Some(group1) = caps.get(1) {
607 group1.as_str().to_string()
608 } else if let Some(group0) = caps.get(0) {
609 group0.as_str().to_string()
610 } else {
611 String::new()
612 }
613 } else {
614 String::new()
615 }
616 } else {
617 String::new()
618 }
619 }
620
621 pub fn is_match(patterns: &[&str], value: &str) -> bool {
623 for pattern in patterns {
624 if let Ok(re) = Regex::new(pattern)
625 && re.is_match(value)
626 {
627 return true;
628 }
629 }
630 false
631 }
632
633 pub fn match_all(pattern: &str, subject: &str) -> Vec<String> {
635 if let Ok(re) = Regex::new(pattern) {
636 re.find_iter(subject)
637 .map(|m| m.as_str().to_string())
638 .collect()
639 } else {
640 Vec::new()
641 }
642 }
643
644 pub fn numbers(value: &str) -> String {
646 value.chars().filter(|c| c.is_numeric()).collect()
647 }
648
649 pub fn pad_both(value: &str, length: usize, pad: &str) -> String {
651 let current_len = Self::length(value);
652 if current_len >= length {
653 return value.to_string();
654 }
655
656 let total_padding = length - current_len;
657 let left_padding = total_padding / 2;
658 let right_padding = total_padding - left_padding;
659
660 let left_pad = pad.repeat(left_padding.div_ceil(pad.len()));
661 let right_pad = pad.repeat(right_padding.div_ceil(pad.len()));
662
663 format!(
664 "{}{}{}",
665 &left_pad[..left_padding.min(left_pad.len())],
666 value,
667 &right_pad[..right_padding.min(right_pad.len())]
668 )
669 }
670
671 pub fn pad_left(value: &str, length: usize, pad: &str) -> String {
673 let current_len = Self::length(value);
674 if current_len >= length {
675 return value.to_string();
676 }
677
678 let padding_needed = length - current_len;
679 let pad_string = pad.repeat(padding_needed.div_ceil(pad.len()));
680
681 format!(
682 "{}{}",
683 &pad_string[..padding_needed.min(pad_string.len())],
684 value
685 )
686 }
687
688 pub fn pad_right(value: &str, length: usize, pad: &str) -> String {
690 let current_len = Self::length(value);
691 if current_len >= length {
692 return value.to_string();
693 }
694
695 let padding_needed = length - current_len;
696 let pad_string = pad.repeat(padding_needed.div_ceil(pad.len()));
697
698 format!(
699 "{}{}",
700 value,
701 &pad_string[..padding_needed.min(pad_string.len())]
702 )
703 }
704
705 pub fn parse_callback(callback: &str, default: Option<&str>) -> (String, Option<String>) {
707 if callback.contains("@anonymous") {
708 let at_count = callback.matches('@').count();
709 if at_count > 1 {
710 let parts: Vec<&str> = callback.rsplitn(2, '@').collect();
711 return (parts[1].to_string(), Some(parts[0].to_string()));
712 }
713 return (callback.to_string(), default.map(|s| s.to_string()));
714 }
715
716 if callback.contains('@') {
717 let parts: Vec<&str> = callback.splitn(2, '@').collect();
718 (parts[0].to_string(), Some(parts[1].to_string()))
719 } else {
720 (callback.to_string(), default.map(|s| s.to_string()))
721 }
722 }
723
724 pub fn plural(value: &str, count: i32, prepend_count: bool) -> String {
726 let plural_form = if count == 1 {
727 value.to_string()
728 } else {
729 for (rule, replacement) in PLURAL_RULES.iter() {
730 if rule.is_match(value) {
731 return if prepend_count {
732 format!("{} {}", count, rule.replace(value, *replacement))
733 } else {
734 rule.replace(value, *replacement).to_string()
735 };
736 }
737 }
738 format!("{}s", value) };
740
741 if prepend_count {
742 format!("{} {}", count, plural_form)
743 } else {
744 plural_form
745 }
746 }
747
748 pub fn plural_studly(value: &str, count: i32) -> String {
750 let re = Regex::new(r"([a-z])([A-Z])").unwrap();
751 let with_spaces = re.replace_all(value, "$1 $2");
752 let parts: Vec<&str> = with_spaces.split_whitespace().collect();
753
754 if let Some(last_word) = parts.last() {
755 let mut result = parts[..parts.len() - 1].join("");
756 result.push_str(&Self::plural(last_word, count, false));
757 result
758 } else {
759 Self::plural(value, count, false)
760 }
761 }
762
763 pub fn plural_pascal(value: &str, count: i32) -> String {
765 Self::plural_studly(value, count)
766 }
767
768 pub fn password(
770 length: usize,
771 letters: bool,
772 numbers: bool,
773 symbols: bool,
774 spaces: bool,
775 ) -> String {
776 let mut chars = Vec::new();
777
778 if letters {
779 chars.extend_from_slice(&[
780 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
781 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F',
782 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V',
783 'W', 'X', 'Y', 'Z',
784 ]);
785 }
786
787 if numbers {
788 chars.extend_from_slice(&['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']);
789 }
790
791 if symbols {
792 chars.extend_from_slice(&[
793 '~', '!', '#', '$', '%', '^', '&', '*', '(', ')', '-', '_', '.', ',', '<', '>',
794 '?', '/', '\\', '{', '}', '[', ']', '|', ':', ';',
795 ]);
796 }
797
798 if spaces {
799 chars.push(' ');
800 }
801
802 if chars.is_empty() {
803 return String::new();
804 }
805
806 let mut rng = thread_rng();
807 (0..length)
808 .map(|_| chars[rng.gen_range(0..chars.len())])
809 .collect()
810 }
811
812 pub fn position(haystack: &str, needle: &str, offset: usize) -> Option<usize> {
814 let chars: Vec<char> = haystack.chars().collect();
815 if offset >= chars.len() {
816 return None;
817 }
818
819 let search_slice: String = chars.iter().skip(offset).collect();
820 if let Some(pos) = search_slice.find(needle) {
821 let chars_before_match: String = chars.iter().take(offset).collect();
822 Some(chars_before_match.len() + pos)
823 } else {
824 None
825 }
826 }
827
828 pub fn random(length: usize) -> String {
830 {
832 let mut sequence = RANDOM_SEQUENCE.lock().unwrap();
833 if let Some((seq, index)) = sequence.as_mut()
834 && *index < seq.len()
835 {
836 let result = seq[*index].clone();
837 *index += 1;
838 return result;
839 }
840 }
841
842 if let Some(factory) = RANDOM_STRING_FACTORY.get() {
843 return factory(length);
844 }
845
846 let charset: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
847 let mut rng = thread_rng();
848
849 (0..length)
850 .map(|_| {
851 let idx = rng.gen_range(0..charset.len());
852 charset[idx] as char
853 })
854 .collect()
855 }
856
857 pub fn create_random_strings_using<F>(factory: F)
859 where
860 F: Fn(usize) -> String + Send + Sync + 'static,
861 {
862 let _ = RANDOM_STRING_FACTORY.set(Box::new(factory));
863 }
864
865 pub fn create_random_strings_using_sequence(sequence: Vec<String>) {
867 let mut seq_guard = RANDOM_SEQUENCE.lock().unwrap();
868 *seq_guard = Some((sequence, 0));
869 }
870
871 pub fn create_random_strings_normally() {
873 let mut seq_guard = RANDOM_SEQUENCE.lock().unwrap();
877 *seq_guard = None;
878 }
879
880 pub fn repeat(string: &str, times: usize) -> String {
882 string.repeat(times)
883 }
884
885 pub fn replace_array(search: &str, replace: &[&str], subject: &str) -> String {
887 let segments: Vec<&str> = subject.split(search).collect();
888 let mut result = segments[0].to_string();
889
890 for (i, segment) in segments.iter().skip(1).enumerate() {
891 let replacement = replace.get(i).unwrap_or(&search);
892 result.push_str(replacement);
893 result.push_str(segment);
894 }
895
896 result
897 }
898
899 pub fn replace(
901 search: &[&str],
902 replace: &[&str],
903 subject: &str,
904 case_sensitive: bool,
905 ) -> String {
906 let mut result = subject.to_string();
907
908 let mut placeholders = Vec::new();
910
911 for (i, &search_term) in search.iter().enumerate() {
912 let placeholder = format!("__PLACEHOLDER_{}__", i);
913 placeholders.push(placeholder.clone());
914
915 if case_sensitive {
916 result = result.replace(search_term, &placeholder);
917 } else {
918 let re = Regex::new(&format!("(?i){}", regex::escape(search_term))).unwrap();
919 result = re.replace_all(&result, placeholder.as_str()).to_string();
920 }
921 }
922
923 for (i, placeholder) in placeholders.iter().enumerate() {
925 let replacement = replace.get(i).unwrap_or(&"");
926 result = result.replace(placeholder, replacement);
927 }
928
929 result
930 }
931
932 pub fn replace_first(search: &str, replace: &str, subject: &str) -> String {
934 if search.is_empty() {
935 return subject.to_string();
936 }
937
938 if let Some(pos) = subject.find(search) {
939 let mut result = String::new();
940 result.push_str(&subject[..pos]);
941 result.push_str(replace);
942 result.push_str(&subject[pos + search.len()..]);
943 result
944 } else {
945 subject.to_string()
946 }
947 }
948
949 pub fn replace_start(search: &str, replace: &str, subject: &str) -> String {
951 if search.is_empty() {
952 return subject.to_string();
953 }
954
955 if subject.starts_with(search) {
956 Self::replace_first(search, replace, subject)
957 } else {
958 subject.to_string()
959 }
960 }
961
962 pub fn replace_last(search: &str, replace: &str, subject: &str) -> String {
964 if search.is_empty() {
965 return subject.to_string();
966 }
967
968 if let Some(pos) = subject.rfind(search) {
969 let mut result = String::new();
970 result.push_str(&subject[..pos]);
971 result.push_str(replace);
972 result.push_str(&subject[pos + search.len()..]);
973 result
974 } else {
975 subject.to_string()
976 }
977 }
978
979 pub fn replace_end(search: &str, replace: &str, subject: &str) -> String {
981 if search.is_empty() {
982 return subject.to_string();
983 }
984
985 if subject.ends_with(search) {
986 Self::replace_last(search, replace, subject)
987 } else {
988 subject.to_string()
989 }
990 }
991
992 pub fn replace_matches(
994 pattern: &str,
995 replace: &str,
996 subject: &str,
997 limit: Option<usize>,
998 ) -> String {
999 if let Ok(re) = Regex::new(pattern) {
1000 match limit {
1001 Some(n) => re.replacen(subject, n, replace).to_string(),
1002 None => re.replace_all(subject, replace).to_string(),
1003 }
1004 } else {
1005 subject.to_string()
1006 }
1007 }
1008
1009 pub fn remove(search: &[&str], subject: &str, case_sensitive: bool) -> String {
1011 let empty_replace: Vec<&str> = search.iter().map(|_| "").collect();
1012 Self::replace(search, &empty_replace, subject, case_sensitive)
1013 }
1014
1015 pub fn reverse(value: &str) -> String {
1017 value.chars().rev().collect()
1018 }
1019
1020 pub fn start(value: &str, prefix: &str) -> String {
1022 let pattern = format!("^({})+", regex::escape(prefix));
1023 let re = Regex::new(&pattern).unwrap();
1024 let trimmed = re.replace_all(value, "");
1025 format!("{}{}", prefix, trimmed)
1026 }
1027
1028 pub fn upper(value: &str) -> String {
1030 value.to_uppercase()
1031 }
1032
1033 pub fn title(value: &str) -> String {
1035 value
1036 .split_whitespace()
1037 .map(Self::ucfirst)
1038 .collect::<Vec<_>>()
1039 .join(" ")
1040 }
1041
1042 pub fn headline(value: &str) -> String {
1044 let re = Regex::new(r"\s+").unwrap();
1045 let words: Vec<&str> = re.split(value).collect();
1046
1047 let processed: Vec<String> = if words.len() > 1 {
1048 words.iter().map(|word| Self::title(word)).collect()
1049 } else {
1050 Self::uc_split(value)
1051 .iter()
1052 .map(|word| Self::title(word))
1053 .collect()
1054 };
1055
1056 let collapsed = Self::replace(
1057 &["-", "_", " "],
1058 &["_", "_", "_"],
1059 &processed.join("_"),
1060 true,
1061 );
1062 let parts: Vec<&str> = collapsed.split('_').filter(|s| !s.is_empty()).collect();
1063 parts.join(" ")
1064 }
1065
1066 pub fn apa(value: &str) -> String {
1068 if value.trim().is_empty() {
1069 return value.to_string();
1070 }
1071
1072 let minor_words = [
1073 "and", "as", "but", "for", "if", "nor", "or", "so", "yet", "a", "an", "the", "at",
1074 "by", "for", "in", "of", "off", "on", "per", "to", "up", "via",
1075 ];
1076
1077 let end_punctuation = ['.', '!', '?', ':', '—', ','];
1078 let words: Vec<&str> = value.split_whitespace().collect();
1079
1080 let mut result = Vec::new();
1081
1082 for (i, word) in words.iter().enumerate() {
1083 let lowercase_word = word.to_lowercase();
1084
1085 if word.contains('-') {
1086 let hyphenated: Vec<&str> = word.split('-').collect();
1087 let processed: Vec<String> = hyphenated
1088 .iter()
1089 .map(|part| {
1090 let lower_part = part.to_lowercase();
1091 if minor_words.contains(&lower_part.as_str()) && part.len() <= 3 {
1092 lower_part
1093 } else {
1094 Self::ucfirst(part)
1095 }
1096 })
1097 .collect();
1098 result.push(processed.join("-"));
1099 } else if minor_words.contains(&lowercase_word.as_str())
1100 && word.len() <= 3
1101 && i > 0
1102 && !end_punctuation.contains(&words[i - 1].chars().last().unwrap_or(' '))
1103 {
1104 result.push(lowercase_word);
1105 } else {
1106 result.push(Self::ucfirst(word));
1107 }
1108 }
1109
1110 result.join(" ")
1111 }
1112
1113 pub fn singular(value: &str) -> String {
1115 for (rule, replacement) in SINGULAR_RULES.iter() {
1116 if rule.is_match(value) {
1117 return rule.replace(value, *replacement).to_string();
1118 }
1119 }
1120 value.to_string()
1121 }
1122
1123 pub fn slug(
1125 title: &str,
1126 separator: &str,
1127 language: Option<&str>,
1128 dictionary: Option<HashMap<String, String>>,
1129 ) -> String {
1130 let mut title = if language.is_some() {
1131 Self::ascii(title)
1132 } else {
1133 title.to_string()
1134 };
1135
1136 let flip = if separator == "-" { "_" } else { "-" };
1138 let pattern = format!("[{}]+", regex::escape(flip));
1139 let re = Regex::new(&pattern).unwrap();
1140 title = re.replace_all(&title, separator).to_string();
1141
1142 if let Some(dict) = dictionary {
1144 for (key, value) in dict {
1145 let replacement = format!("{}{}{}", separator, value, separator);
1146 title = title.replace(&key, &replacement);
1147 }
1148 } else {
1149 let replacement = format!("{}at{}", separator, separator);
1151 title = title.replace("@", &replacement);
1152 }
1153
1154 let pattern = format!("[^{}\\p{{L}}\\p{{N}}\\s]+", regex::escape(separator));
1156 let re = Regex::new(&pattern).unwrap();
1157 title = re.replace_all(&Self::lower(&title), "").to_string();
1158
1159 let pattern = format!("[{}\\s]+", regex::escape(separator));
1161 let re = Regex::new(&pattern).unwrap();
1162 title = re.replace_all(&title, separator).to_string();
1163
1164 title
1165 .trim_matches(separator.chars().next().unwrap_or('-'))
1166 .to_string()
1167 }
1168
1169 pub fn snake(value: &str, delimiter: &str) -> String {
1171 let cache_key = format!("{}:{}", value, delimiter);
1172 let cache = SNAKE_CACHE.lock().unwrap();
1173 if let Some(cached) = cache.get(&cache_key) {
1174 return cached.clone();
1175 }
1176 drop(cache);
1177
1178 if value
1179 .chars()
1180 .all(|c| c.is_lowercase() || !c.is_alphabetic())
1181 {
1182 let mut cache = SNAKE_CACHE.lock().unwrap();
1183 cache.insert(cache_key, value.to_string());
1184 return value.to_string();
1185 }
1186
1187 let no_spaces = value.replace(' ', delimiter);
1189
1190 let re = Regex::new(r"([a-z])([A-Z])").unwrap();
1191 let with_delim = re.replace_all(&no_spaces, |caps: ®ex::Captures| {
1192 let first = &caps[1];
1193 let second = &caps[2];
1194
1195 format!("{}{}{}", first, delimiter, second)
1196 });
1197
1198 let result = with_delim.to_lowercase();
1199
1200 let mut cache = SNAKE_CACHE.lock().unwrap();
1201 cache.insert(cache_key, result.clone());
1202 result
1203 }
1204
1205 pub fn trim(value: &str) -> String {
1207 Self::trim_custom(value, None)
1208 }
1209
1210 pub fn trim_custom(value: &str, charlist: Option<&str>) -> String {
1212 match charlist {
1213 Some(chars) => value.trim_matches(|c| chars.contains(c)).to_string(),
1214 None => {
1215 let invisible_chars: Vec<char> = INVISIBLE_CHARACTERS.chars().collect();
1216 let mut result = value.to_string();
1217
1218 while let Some(ch) = result.chars().next() {
1220 if ch.is_whitespace() || invisible_chars.contains(&ch) {
1221 result = result[ch.len_utf8()..].to_string();
1222 } else {
1223 break;
1224 }
1225 }
1226
1227 while let Some(ch) = result.chars().last() {
1229 if ch.is_whitespace() || invisible_chars.contains(&ch) {
1230 let mut chars = result.chars();
1231 chars.next_back();
1232 result = chars.as_str().to_string();
1233 } else {
1234 break;
1235 }
1236 }
1237
1238 result
1239 }
1240 }
1241 }
1242
1243 pub fn ltrim(value: &str, charlist: Option<&str>) -> String {
1245 match charlist {
1246 Some(chars) => value.trim_start_matches(|c| chars.contains(c)).to_string(),
1247 None => {
1248 let invisible_chars: Vec<char> = INVISIBLE_CHARACTERS.chars().collect();
1249 let mut result = value.to_string();
1250
1251 while let Some(ch) = result.chars().next() {
1252 if ch.is_whitespace() || invisible_chars.contains(&ch) {
1253 result = result[ch.len_utf8()..].to_string();
1254 } else {
1255 break;
1256 }
1257 }
1258
1259 result
1260 }
1261 }
1262 }
1263
1264 pub fn rtrim(value: &str, charlist: Option<&str>) -> String {
1266 match charlist {
1267 Some(chars) => value.trim_end_matches(|c| chars.contains(c)).to_string(),
1268 None => {
1269 let invisible_chars: Vec<char> = INVISIBLE_CHARACTERS.chars().collect();
1270 let mut result = value.to_string();
1271
1272 while let Some(ch) = result.chars().last() {
1273 if ch.is_whitespace() || invisible_chars.contains(&ch) {
1274 let mut chars = result.chars();
1275 chars.next_back();
1276 result = chars.as_str().to_string();
1277 } else {
1278 break;
1279 }
1280 }
1281
1282 result
1283 }
1284 }
1285 }
1286
1287 pub fn squish(value: &str) -> String {
1289 let trimmed = Self::trim(value);
1290 let re = Regex::new(r"\s+").unwrap();
1291 re.replace_all(&trimmed, " ").to_string()
1292 }
1293
1294 pub fn starts_with(haystack: &str, needles: &[&str]) -> bool {
1296 for needle in needles {
1297 if !needle.is_empty() && haystack.starts_with(needle) {
1298 return true;
1299 }
1300 }
1301 false
1302 }
1303
1304 pub fn doesnt_start_with(haystack: &str, needles: &[&str]) -> bool {
1306 !Self::starts_with(haystack, needles)
1307 }
1308
1309 pub fn studly(value: &str) -> String {
1311 let cache = STUDLY_CACHE.lock().unwrap();
1312 if let Some(cached) = cache.get(value) {
1313 return cached.clone();
1314 }
1315 drop(cache);
1316
1317 let cleaned = value.replace(['-', '_'], " ");
1318 let words: Vec<&str> = cleaned.split_whitespace().collect();
1319 let result: String = words.iter().map(|word| Self::ucfirst(word)).collect();
1320
1321 let mut cache = STUDLY_CACHE.lock().unwrap();
1322 cache.insert(value.to_string(), result.clone());
1323 result
1324 }
1325
1326 pub fn pascal(value: &str) -> String {
1328 Self::studly(value)
1329 }
1330
1331 pub fn substr(string: &str, start: isize, length: Option<usize>) -> String {
1333 let chars: Vec<char> = string.chars().collect();
1334 let str_len = chars.len() as isize;
1335
1336 let start_index = if start < 0 {
1337 if start < -str_len {
1338 0
1339 } else {
1340 (str_len + start) as usize
1341 }
1342 } else {
1343 if start >= str_len {
1344 return String::new();
1345 }
1346 start as usize
1347 };
1348
1349 match length {
1350 Some(len) => {
1351 let end_index = std::cmp::min(start_index + len, chars.len());
1352 chars
1353 .iter()
1354 .skip(start_index)
1355 .take(end_index - start_index)
1356 .collect()
1357 }
1358 None => chars.iter().skip(start_index).collect(),
1359 }
1360 }
1361
1362 pub fn substr_count(
1364 haystack: &str,
1365 needle: &str,
1366 offset: usize,
1367 length: Option<usize>,
1368 ) -> usize {
1369 let search_slice: String = match length {
1370 Some(len) => {
1371 let chars: Vec<char> = haystack.chars().collect();
1372 let end_index = std::cmp::min(offset + len, chars.len());
1373 if offset >= chars.len() {
1374 return 0;
1375 }
1376 chars.iter().skip(offset).take(end_index - offset).collect()
1377 }
1378 None => {
1379 let chars: Vec<char> = haystack.chars().collect();
1380 if offset >= chars.len() {
1381 return 0;
1382 }
1383 chars.iter().skip(offset).collect()
1384 }
1385 };
1386
1387 search_slice.matches(needle).count()
1388 }
1389
1390 pub fn substr_replace(
1392 string: &str,
1393 replace: &str,
1394 offset: isize,
1395 length: Option<usize>,
1396 ) -> String {
1397 let chars: Vec<char> = string.chars().collect();
1398 let str_len = chars.len() as isize;
1399
1400 let start_index = if offset < 0 {
1401 if offset < -str_len {
1402 0
1403 } else {
1404 (str_len + offset) as usize
1405 }
1406 } else {
1407 if offset >= str_len {
1408 return format!("{}{}", string, replace);
1409 }
1410 offset as usize
1411 };
1412
1413 let replace_length = length.unwrap_or(chars.len() - start_index);
1414 let end_index = std::cmp::min(start_index + replace_length, chars.len());
1415
1416 let start: String = chars.iter().take(start_index).collect();
1417 let end: String = chars.iter().skip(end_index).collect();
1418
1419 format!("{}{}{}", start, replace, end)
1420 }
1421
1422 pub fn swap(map: HashMap<String, String>, subject: &str) -> String {
1424 let mut result = subject.to_string();
1425 for (search, replace) in map {
1426 result = result.replace(&search, &replace);
1427 }
1428 result
1429 }
1430
1431 pub fn take(string: &str, limit: isize) -> String {
1433 if limit < 0 {
1434 Self::substr(string, limit, None)
1435 } else {
1436 Self::substr(string, 0, Some(limit as usize))
1437 }
1438 }
1439
1440 pub fn to_base64(string: &str) -> String {
1442 general_purpose::STANDARD.encode(string.as_bytes())
1443 }
1444
1445 pub fn from_base64(string: &str, strict: bool) -> Result<String, base64::DecodeError> {
1447 let engine = if strict {
1448 general_purpose::STANDARD_NO_PAD
1449 } else {
1450 general_purpose::STANDARD
1451 };
1452
1453 let bytes = engine.decode(string)?;
1454 Ok(String::from_utf8_lossy(&bytes).to_string())
1455 }
1456
1457 pub fn lcfirst(string: &str) -> String {
1459 let mut chars = string.chars();
1460 match chars.next() {
1461 None => String::new(),
1462 Some(first) => first.to_lowercase().collect::<String>() + chars.as_str(),
1463 }
1464 }
1465
1466 pub fn ucfirst(string: &str) -> String {
1468 let mut chars = string.chars();
1469 match chars.next() {
1470 None => String::new(),
1471 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
1472 }
1473 }
1474
1475 pub fn uc_split(string: &str) -> Vec<String> {
1477 let mut result = Vec::new();
1478 let mut current = String::new();
1479
1480 for ch in string.chars() {
1481 if ch.is_uppercase() && !current.is_empty() {
1482 result.push(current);
1483 current = String::new();
1484 }
1485 current.push(ch);
1486 }
1487
1488 if !current.is_empty() {
1489 result.push(current);
1490 }
1491
1492 result
1493 }
1494
1495 pub fn word_count(string: &str, characters: Option<&str>) -> usize {
1497 if let Some(chars) = characters {
1498 let pattern = format!("[{}\\s]+", regex::escape(chars));
1500 let re = Regex::new(&pattern).unwrap();
1501 re.split(string).filter(|s| !s.is_empty()).count()
1502 } else {
1503 string.split_whitespace().count()
1504 }
1505 }
1506
1507 pub fn word_wrap(
1509 string: &str,
1510 characters: usize,
1511 break_str: &str,
1512 cut_long_words: bool,
1513 ) -> String {
1514 if cut_long_words {
1515 textwrap::fill(string, characters)
1516 } else {
1517 let options = textwrap::Options::new(characters).break_words(false);
1518 textwrap::fill(string, options)
1519 }
1520 .replace('\n', break_str)
1521 }
1522
1523 pub fn uuid() -> Uuid {
1525 {
1527 let mut sequence = UUID_SEQUENCE.lock().unwrap();
1528 if let Some((seq, index)) = sequence.as_mut()
1529 && *index < seq.len()
1530 {
1531 let result = seq[*index];
1532 *index += 1;
1533 return result;
1534 }
1535 }
1536
1537 if let Some(factory) = UUID_FACTORY.get() {
1538 return factory();
1539 }
1540 Uuid::new_v4()
1541 }
1542
1543 pub fn uuid7(time: Option<DateTime<Utc>>) -> Uuid {
1545 if let Some(factory) = UUID_FACTORY.get() {
1546 return factory();
1547 }
1548
1549 match time {
1550 Some(_) => Uuid::new_v4(), None => Uuid::new_v4(),
1552 }
1553 }
1554
1555 pub fn ordered_uuid() -> Uuid {
1557 if let Some(factory) = UUID_FACTORY.get() {
1558 return factory();
1559 }
1560
1561 Uuid::new_v4()
1563 }
1564
1565 pub fn create_uuids_using<F>(factory: F)
1567 where
1568 F: Fn() -> Uuid + Send + Sync + 'static,
1569 {
1570 let _ = UUID_FACTORY.set(Box::new(factory));
1571 }
1572
1573 pub fn create_uuids_using_sequence(sequence: Vec<Uuid>) {
1575 let mut seq_guard = UUID_SEQUENCE.lock().unwrap();
1576 *seq_guard = Some((sequence, 0));
1577 }
1578
1579 pub fn freeze_uuids<F>(callback: Option<F>) -> Uuid
1581 where
1582 F: FnOnce(Uuid),
1583 {
1584 let uuid = Uuid::new_v4();
1585 Self::create_uuids_using(move || uuid);
1586
1587 if let Some(cb) = callback {
1588 cb(uuid);
1589 Self::create_uuids_normally();
1590 }
1591
1592 uuid
1593 }
1594
1595 pub fn create_uuids_normally() {
1597 let mut seq_guard = UUID_SEQUENCE.lock().unwrap();
1599 *seq_guard = None;
1600 }
1601
1602 pub fn ulid(time: Option<DateTime<Utc>>) -> Ulid {
1604 {
1606 let mut sequence = ULID_SEQUENCE.lock().unwrap();
1607 if let Some((seq, index)) = sequence.as_mut()
1608 && *index < seq.len()
1609 {
1610 let result = seq[*index];
1611 *index += 1;
1612 return result;
1613 }
1614 }
1615
1616 if let Some(factory) = ULID_FACTORY.get() {
1617 return factory();
1618 }
1619
1620 match time {
1621 Some(_) => Ulid::new(), None => Ulid::new(),
1623 }
1624 }
1625
1626 pub fn create_ulids_normally() {
1628 let mut seq_guard = ULID_SEQUENCE.lock().unwrap();
1629 *seq_guard = None;
1630 }
1631
1632 pub fn create_ulids_using<F>(factory: F)
1634 where
1635 F: Fn() -> Ulid + Send + Sync + 'static,
1636 {
1637 let _ = ULID_FACTORY.set(Box::new(factory));
1638 }
1639
1640 pub fn create_ulids_using_sequence(sequence: Vec<Ulid>) {
1642 let mut seq_guard = ULID_SEQUENCE.lock().unwrap();
1643 *seq_guard = Some((sequence, 0));
1644 }
1645
1646 pub fn freeze_ulids<F>(callback: Option<F>) -> Ulid
1648 where
1649 F: FnOnce(Ulid),
1650 {
1651 let ulid = Ulid::new();
1652 Self::create_ulids_using(move || ulid);
1653
1654 if let Some(cb) = callback {
1655 cb(ulid);
1656 Self::create_ulids_normally();
1657 }
1658
1659 ulid
1660 }
1661
1662 pub fn flush_cache() {
1664 let mut snake_cache = SNAKE_CACHE.lock().unwrap();
1665 let mut camel_cache = CAMEL_CACHE.lock().unwrap();
1666 let mut studly_cache = STUDLY_CACHE.lock().unwrap();
1667
1668 snake_cache.clear();
1669 camel_cache.clear();
1670 studly_cache.clear();
1671 }
1672}
1673
1674#[derive(Debug, Clone, Copy)]
1677pub enum CaseMode {
1678 Upper,
1679 Lower,
1680 Title,
1681 Fold,
1682}
1683
1684#[derive(Debug, Clone)]
1685pub struct ExcerptOptions {
1686 pub radius: usize,
1687 pub omission: String,
1688}
1689
1690impl Default for ExcerptOptions {
1691 fn default() -> Self {
1692 Self {
1693 radius: 100,
1694 omission: "...".to_string(),
1695 }
1696 }
1697}
1698
1699#[derive(Debug, Clone)]
1700pub struct MarkdownOptions {
1701 pub tables: bool,
1702 pub footnotes: bool,
1703 pub strikethrough: bool,
1704 pub tasklists: bool,
1705}
1706
1707impl Default for MarkdownOptions {
1708 fn default() -> Self {
1709 Self {
1710 tables: true,
1711 footnotes: true,
1712 strikethrough: true,
1713 tasklists: true,
1714 }
1715 }
1716}
1717
1718#[derive(Debug, Clone, Copy)]
1719pub enum UuidVersion {
1720 Nil,
1721 V1,
1722 V3,
1723 V4,
1724 V5,
1725 Max,
1726}
1727
1728#[cfg(test)]
1729mod tests {
1730 use super::*;
1731
1732 #[test]
1733 fn test_after() {
1734 assert_eq!(Str::after("This is my name", "This is"), " my name");
1735 assert_eq!(Str::after("App\\Class@method", "@"), "method");
1736 }
1737
1738 #[test]
1739 fn test_after_last() {
1740 assert_eq!(Str::after_last("yvette@yvette@gmail.com", "@"), "gmail.com");
1741 }
1742
1743 #[test]
1744 fn test_ascii() {
1745 assert_eq!(Str::ascii("ñ"), "n");
1746 assert_eq!(Str::ascii("üñíçødé"), "unicode");
1747 }
1748
1749 #[test]
1750 fn test_before() {
1751 assert_eq!(Str::before("This is my name", "my name"), "This is ");
1752 assert_eq!(Str::before("App\\Class@method", "@"), "App\\Class");
1753 }
1754
1755 #[test]
1756 fn test_before_last() {
1757 assert_eq!(
1758 Str::before_last("yvette@yvette@gmail.com", "@"),
1759 "yvette@yvette"
1760 );
1761 }
1762
1763 #[test]
1764 fn test_between() {
1765 assert_eq!(Str::between("This is my name", "This", "name"), " is my ");
1766 assert_eq!(Str::between("App\\Class@method", "\\", "@"), "Class");
1767 }
1768
1769 #[test]
1770 fn test_between_first() {
1771 assert_eq!(Str::between_first("[a] bc [d]", "[", "]"), "a");
1772 }
1773
1774 #[test]
1775 fn test_camel() {
1776 assert_eq!(Str::camel("foo_bar"), "fooBar");
1777 assert_eq!(Str::camel("foo-bar"), "fooBar");
1778 assert_eq!(Str::camel("FooBar"), "fooBar");
1779 }
1780
1781 #[test]
1782 fn test_char_at() {
1783 assert_eq!(Str::char_at("This is my name", 6), Some('s'));
1784 assert_eq!(Str::char_at("This is my name", -1), Some('e'));
1785 assert_eq!(Str::char_at("This is my name", 100), None);
1786 }
1787
1788 #[test]
1789 fn test_contains() {
1790 assert!(Str::contains("This is my name", &["my"], false));
1791 assert!(!Str::contains("This is my name", &["hello"], false));
1792 assert!(Str::contains("This is my name", &["MY"], true));
1793 }
1794
1795 #[test]
1796 fn test_contains_all() {
1797 assert!(Str::contains_all("This is my name", &["This", "my"], false));
1798 assert!(!Str::contains_all(
1799 "This is my name",
1800 &["This", "hello"],
1801 false
1802 ));
1803 }
1804
1805 #[test]
1806 fn test_deduplicate() {
1807 assert_eq!(Str::deduplicate("Hello World", &[' ']), "Hello World");
1808 assert_eq!(Str::deduplicate("aabbcc", &['a', 'b']), "abcc");
1809 }
1810
1811 #[test]
1812 fn test_ends_with() {
1813 assert!(Str::ends_with("Hello World", &["World"]));
1814 assert!(!Str::ends_with("Hello World", &["Hello"]));
1815 }
1816
1817 #[test]
1818 fn test_excerpt() {
1819 let options = ExcerptOptions::default();
1820 let result = Str::excerpt(
1821 "This is a very long string that contains the word Laravel somewhere in the middle",
1822 "Laravel",
1823 options,
1824 );
1825 assert!(result.is_some());
1826 assert!(result.unwrap().contains("Laravel"));
1827 }
1828
1829 #[test]
1830 fn test_finish() {
1831 assert_eq!(Str::finish("this/string", "/"), "this/string/");
1832 assert_eq!(Str::finish("this/string/", "/"), "this/string/");
1833 }
1834
1835 #[test]
1836 fn test_is_ascii() {
1837 assert!(Str::is_ascii("Taylor"));
1838 assert!(!Str::is_ascii("ñ"));
1839 }
1840
1841 #[test]
1842 fn test_is_json() {
1843 assert!(Str::is_json(r#"{"name":"John","age":30}"#));
1844 assert!(!Str::is_json("not json"));
1845 }
1846
1847 #[test]
1848 fn test_is_url() {
1849 assert!(Str::is_url("https://laravel.com", None));
1850 assert!(!Str::is_url("not a url", None));
1851 }
1852
1853 #[test]
1854 fn test_is_uuid() {
1855 assert!(Str::is_uuid("a0a2a2d2-0b87-4a18-83f2-2529882be2de", None));
1856 assert!(!Str::is_uuid("not a uuid", None));
1857 }
1858
1859 #[test]
1860 fn test_kebab() {
1861 assert_eq!(Str::kebab("fooBar"), "foo-bar");
1862 }
1863
1864 #[test]
1865 fn test_length() {
1866 assert_eq!(Str::length("Laravel"), 7);
1867 assert_eq!(Str::length("😀😃😄😁"), 4);
1868 }
1869
1870 #[test]
1871 fn test_limit() {
1872 assert_eq!(
1873 Str::limit(
1874 "The quick brown fox jumps over the lazy dog",
1875 20,
1876 "...",
1877 false
1878 ),
1879 "The quick brown fox ..."
1880 );
1881 }
1882
1883 #[test]
1884 fn test_lower() {
1885 assert_eq!(Str::lower("LARAVEL"), "laravel");
1886 }
1887
1888 #[test]
1889 fn test_mask() {
1890 assert_eq!(
1891 Str::mask("taylor@example.com", '*', 3, Some(10)),
1892 "tay**********e.com"
1893 );
1894 }
1895
1896 #[test]
1897 fn test_numbers() {
1898 assert_eq!(Str::numbers("Laravel 9.0"), "90");
1899 }
1900
1901 #[test]
1902 fn test_pad_left() {
1903 assert_eq!(Str::pad_left("James", 10, " "), " James");
1904 }
1905
1906 #[test]
1907 fn test_pad_right() {
1908 assert_eq!(Str::pad_right("James", 10, "-"), "James-----");
1909 }
1910
1911 #[test]
1912 fn test_plural() {
1913 assert_eq!(Str::plural("car", 1, false), "car");
1914 assert_eq!(Str::plural("car", 2, false), "cars");
1915 assert_eq!(Str::plural("child", 2, false), "children");
1916 }
1917
1918 #[test]
1919 fn test_random() {
1920 let random1 = Str::random(10);
1921 let random2 = Str::random(10);
1922 assert_eq!(random1.len(), 10);
1923 assert_eq!(random2.len(), 10);
1924 assert_ne!(random1, random2);
1925 }
1926
1927 #[test]
1928 fn test_remove() {
1929 assert_eq!(
1930 Str::remove(
1931 &["e"],
1932 "Peter Piper picked a peck of pickled peppers.",
1933 true
1934 ),
1935 "Ptr Pipr pickd a pck of pickld ppprs."
1936 );
1937 }
1938
1939 #[test]
1940 fn test_replace() {
1941 assert_eq!(
1942 Str::replace(&["9.x", "10.x"], &["10.x", "11.x"], "Laravel 9.x", true),
1943 "Laravel 10.x"
1944 );
1945 }
1946
1947 #[test]
1948 fn test_replace_first() {
1949 assert_eq!(
1950 Str::replace_first("the", "a", "the quick brown fox jumps over the lazy dog"),
1951 "a quick brown fox jumps over the lazy dog"
1952 );
1953 }
1954
1955 #[test]
1956 fn test_replace_last() {
1957 assert_eq!(
1958 Str::replace_last("the", "a", "the quick brown fox jumps over the lazy dog"),
1959 "the quick brown fox jumps over a lazy dog"
1960 );
1961 }
1962
1963 #[test]
1964 fn test_reverse() {
1965 assert_eq!(Str::reverse("Hello World"), "dlroW olleH");
1966 }
1967
1968 #[test]
1969 fn test_singular() {
1970 assert_eq!(Str::singular("cars"), "car");
1971 assert_eq!(Str::singular("children"), "child");
1972 }
1973
1974 #[test]
1975 fn test_slug() {
1976 assert_eq!(
1977 Str::slug("Laravel 5 Framework", "-", None, None),
1978 "laravel-5-framework"
1979 );
1980 }
1981
1982 #[test]
1983 fn test_snake() {
1984 assert_eq!(Str::snake("fooBar", "_"), "foo_bar");
1985 assert_eq!(Str::snake("FooBar", "_"), "foo_bar");
1986 }
1987
1988 #[test]
1989 fn test_start() {
1990 assert_eq!(Str::start("this/string", "/"), "/this/string");
1991 assert_eq!(Str::start("/this/string", "/"), "/this/string");
1992 }
1993
1994 #[test]
1995 fn test_starts_with() {
1996 assert!(Str::starts_with("Hello World", &["Hello"]));
1997 assert!(!Str::starts_with("Hello World", &["World"]));
1998 }
1999
2000 #[test]
2001 fn test_studly() {
2002 assert_eq!(Str::studly("foo_bar"), "FooBar");
2003 assert_eq!(Str::studly("foo-bar"), "FooBar");
2004 }
2005
2006 #[test]
2007 fn test_substr() {
2008 assert_eq!(Str::substr("The Laravel Framework", 4, Some(7)), "Laravel");
2009 assert_eq!(
2010 Str::substr("The Laravel Framework", -9, Some(9)),
2011 "Framework"
2012 );
2013 }
2014
2015 #[test]
2016 fn test_title() {
2017 assert_eq!(
2018 Str::title("a nice title uses the correct case"),
2019 "A Nice Title Uses The Correct Case"
2020 );
2021 }
2022
2023 #[test]
2024 fn test_ucfirst() {
2025 assert_eq!(Str::ucfirst("foo bar"), "Foo bar");
2026 }
2027
2028 #[test]
2029 fn test_upper() {
2030 assert_eq!(Str::upper("laravel"), "LARAVEL");
2031 }
2032
2033 #[test]
2034 fn test_uuid() {
2035 let uuid1 = Str::uuid();
2036 let uuid2 = Str::uuid();
2037 assert_ne!(uuid1, uuid2);
2038 assert!(Str::is_uuid(&uuid1.to_string(), None));
2039 }
2040
2041 #[test]
2042 fn test_word_count() {
2043 assert_eq!(Str::word_count("Hello, how are you?", None), 4);
2044 }
2045
2046 #[test]
2047 fn test_words() {
2048 assert_eq!(
2049 Str::words("Perfectly balanced, as all things should be.", 3, ">>>"),
2050 "Perfectly balanced, as>>>"
2051 );
2052 }
2053}