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