Skip to main content

illuminate_string/
lib.rs

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; // <-- The path changes here
8use std::sync::{Arc, Mutex, OnceLock};
9
10use ulid::Ulid;
11use unicode_segmentation::UnicodeSegmentation;
12use unidecode::unidecode;
13use uuid::Uuid;
14
15// Type aliases for complex types
16type UuidSequence = Arc<Mutex<Option<(Vec<Uuid>, usize)>>>;
17type UlidSequence = Arc<Mutex<Option<(Vec<Ulid>, usize)>>>;
18type RandomSequence = Arc<Mutex<Option<(Vec<String>, usize)>>>;
19
20// Global caches for performance
21static 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
28// Factory functions
29static 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
34// Sequence factories
35static 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
39// Invisible characters constant
40const 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
42// Pluralization rules (simplified English rules)
43static 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    /// Return the remainder of a string after the first occurrence of a given value
119    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    /// Return the remainder of a string after the last occurrence of a given value
132    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    /// Transliterate a UTF-8 value to ASCII
145    pub fn ascii(value: &str) -> String {
146        unidecode(value)
147    }
148
149    /// Transliterate a string to its closest ASCII representation
150    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    /// Get the portion of a string before the first occurrence of a given value
174    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    /// Get the portion of a string before the last occurrence of a given value
187    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    /// Get the portion of a string between two given values
200    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    /// Get the smallest possible portion of a string between two given values
210    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    /// Convert a value to camel case
220    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    /// Get the character at the specified index
236    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    /// Remove the given string(s) if it exists at the start of the haystack
256    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    /// Remove the given string(s) if it exists at the end of the haystack
266    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    /// Determine if a given string contains a given substring
276    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    /// Determine if a given string contains all array values
302    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    /// Determine if a given string doesn't contain a given substring
312    pub fn doesnt_contain(haystack: &str, needles: &[&str], ignore_case: bool) -> bool {
313        !Self::contains(haystack, needles, ignore_case)
314    }
315
316    /// Convert the case of a string
317    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(), // Case folding approximation
323        }
324    }
325
326    /// Replace consecutive instances of a given character with a single character
327    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    /// Determine if a given string ends with a given substring
340    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    /// Determine if a given string doesn't end with a given substring
350    pub fn doesnt_end_with(haystack: &str, needles: &[&str]) -> bool {
351        !Self::ends_with(haystack, needles)
352    }
353
354    /// Extracts an excerpt from text that matches the first instance of a phrase
355    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    /// Cap a string with a single instance of a given value
389    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    /// Wrap the string with the given strings
397    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    /// Unwrap the string with the given strings
403    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    /// Determine if a given string matches a given pattern (with wildcards)
419    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    /// Determine if a given string is 7 bit ASCII
444    pub fn is_ascii(value: &str) -> bool {
445        value.is_ascii()
446    }
447
448    /// Determine if a given value is valid JSON
449    pub fn is_json(value: &str) -> bool {
450        serde_json::from_str::<serde_json::Value>(value).is_ok()
451    }
452
453    /// Determine if a given value is a valid URL
454    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    /// Determine if a given value is a valid UUID
467    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    /// Determine if a given value is a valid ULID
484    pub fn is_ulid(value: &str) -> bool {
485        Ulid::from_string(value).is_ok()
486    }
487
488    /// Convert a string to kebab case
489    pub fn kebab(value: &str) -> String {
490        Self::snake(value, "-")
491    }
492
493    /// Return the length of the given string
494    pub fn length(value: &str) -> usize {
495        value.graphemes(true).count()
496    }
497
498    /// Limit the number of characters in a string
499    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        // For word preservation, we need to work with the actual string
512        let truncated: String = graphemes.iter().take(limit).copied().collect();
513        let trimmed = truncated.trim_end();
514
515        // Find the last space and truncate there
516        if let Some(last_space) = trimmed.rfind(' ') {
517            format!("{}{}", &trimmed[..last_space], end)
518        } else {
519            format!("{}{}", trimmed, end)
520        }
521    }
522
523    /// Convert the given string to lower-case
524    pub fn lower(value: &str) -> String {
525        value.to_lowercase()
526    }
527
528    /// Limit the number of words in a string
529    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    /// Converts GitHub flavored Markdown into HTML
541    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    /// Converts inline Markdown into HTML
563    pub fn inline_markdown(string: &str, options: MarkdownOptions) -> String {
564        // For inline markdown, we strip block elements
565        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    /// Masks a portion of a string with a repeated character
571    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    /// Get the string matching the given pattern
603    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    /// Determine if a given string matches a given pattern
622    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    /// Get all strings matching the given pattern
634    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    /// Remove all non-numeric characters from a string
645    pub fn numbers(value: &str) -> String {
646        value.chars().filter(|c| c.is_numeric()).collect()
647    }
648
649    /// Pad both sides of a string with another
650    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    /// Pad the left side of a string with another
672    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    /// Pad the right side of a string with another
689    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    /// Parse a Class[@]method style callback into class and method
706    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    /// Get the plural form of an English word
725    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) // fallback
739        };
740
741        if prepend_count {
742            format!("{} {}", count, plural_form)
743        } else {
744            plural_form
745        }
746    }
747
748    /// Pluralize the last word of an English, studly caps case string
749    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    /// Pluralize the last word of an English, Pascal caps case string
764    pub fn plural_pascal(value: &str, count: i32) -> String {
765        Self::plural_studly(value, count)
766    }
767
768    /// Generate a random, secure password
769    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    /// Find the multi-byte safe position of the first occurrence of a given substring
813    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    /// Generate a more truly "random" alpha-numeric string
829    pub fn random(length: usize) -> String {
830        // Check for sequence first
831        {
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    /// Set the callable that will be used to generate random strings
858    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    /// Set the sequence that will be used to generate random strings
866    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    /// Indicate that random strings should be created normally
872    pub fn create_random_strings_normally() {
873        // Clear the factory and sequence
874        // Note: OnceLock doesn't have a clear method, so we'd need to restructure
875        // For now, we'll clear the sequence
876        let mut seq_guard = RANDOM_SEQUENCE.lock().unwrap();
877        *seq_guard = None;
878    }
879
880    /// Repeat the given string
881    pub fn repeat(string: &str, times: usize) -> String {
882        string.repeat(times)
883    }
884
885    /// Replace a given value in the string sequentially with an array
886    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    /// Replace the given value in the given string
900    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        // Use a unique placeholder to avoid double replacement
909        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        // Replace placeholders with actual replacements
924        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    /// Replace the first occurrence of a given value in the string
933    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    /// Replace the first occurrence of the given value if it appears at the start
950    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    /// Replace the last occurrence of a given value in the string
963    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    /// Replace the last occurrence of a given value if it appears at the end
980    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    /// Replace the patterns matching the given regular expression
993    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    /// Remove any occurrence of the given string in the subject
1010    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    /// Reverse the given string
1016    pub fn reverse(value: &str) -> String {
1017        value.chars().rev().collect()
1018    }
1019
1020    /// Begin a string with a single instance of a given value
1021    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    /// Convert the given string to upper-case
1029    pub fn upper(value: &str) -> String {
1030        value.to_uppercase()
1031    }
1032
1033    /// Convert the given string to title case
1034    pub fn title(value: &str) -> String {
1035        value
1036            .split_whitespace()
1037            .map(Self::ucfirst)
1038            .collect::<Vec<_>>()
1039            .join(" ")
1040    }
1041
1042    /// Convert the given string to headline case
1043    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    /// Convert the given string to APA-style title case
1067    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    /// Get the singular form of an English word
1114    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    /// Generate a URL friendly "slug" from a given string
1124    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        // Convert all dashes/underscores into separator
1137        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        // Replace dictionary words
1143        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            // Default dictionary
1150            let replacement = format!("{}at{}", separator, separator);
1151            title = title.replace("@", &replacement);
1152        }
1153
1154        // Remove all characters that are not the separator, letters, numbers, or whitespace
1155        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        // Replace all separator characters and whitespace by a single separator
1160        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    /// Convert a string to snake case
1170    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        // Replace spaces with delimiter, then handle camelCase
1188        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: &regex::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    /// Remove all whitespace from both ends of a string
1206    pub fn trim(value: &str) -> String {
1207        Self::trim_custom(value, None)
1208    }
1209
1210    /// Remove all whitespace from both ends of a string with custom character list
1211    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                // Trim from start
1219                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                // Trim from end
1228                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    /// Remove all whitespace from the beginning of a string
1244    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    /// Remove all whitespace from the end of a string
1265    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    /// Remove all "extra" blank space from the given string
1288    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    /// Determine if a given string starts with a given substring
1295    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    /// Determine if a given string doesn't start with a given substring
1305    pub fn doesnt_start_with(haystack: &str, needles: &[&str]) -> bool {
1306        !Self::starts_with(haystack, needles)
1307    }
1308
1309    /// Convert a value to studly caps case
1310    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    /// Convert a value to Pascal case (alias for studly)
1327    pub fn pascal(value: &str) -> String {
1328        Self::studly(value)
1329    }
1330
1331    /// Returns the portion of the string specified by the start and length parameters
1332    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    /// Returns the number of substring occurrences
1363    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    /// Replace text within a portion of a string
1391    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    /// Swap multiple keywords in a string with other keywords
1423    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    /// Take the first or last {$limit} characters of a string
1432    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    /// Convert the given string to Base64 encoding
1441    pub fn to_base64(string: &str) -> String {
1442        general_purpose::STANDARD.encode(string.as_bytes())
1443    }
1444
1445    /// Decode the given Base64 encoded string
1446    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    /// Make a string's first character lowercase
1458    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    /// Make a string's first character uppercase
1467    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    /// Split a string into pieces by uppercase characters
1476    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    /// Get the number of words a string contains
1496    pub fn word_count(string: &str, characters: Option<&str>) -> usize {
1497        if let Some(chars) = characters {
1498            // Custom word character definition
1499            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    /// Wrap a string to a given number of characters
1508    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    /// Generate a UUID (version 4)
1524    pub fn uuid() -> Uuid {
1525        // Check for sequence first
1526        {
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    /// Generate a UUID (version 7)
1544    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(), // Simplified - would need uuid v7 implementation
1551            None => Uuid::new_v4(),
1552        }
1553    }
1554
1555    /// Generate a time-ordered UUID
1556    pub fn ordered_uuid() -> Uuid {
1557        if let Some(factory) = UUID_FACTORY.get() {
1558            return factory();
1559        }
1560
1561        // Simplified implementation - in reality would use COMB UUID
1562        Uuid::new_v4()
1563    }
1564
1565    /// Set the callable that will be used to generate UUIDs
1566    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    /// Set the sequence that will be used to generate UUIDs
1574    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    /// Always return the same UUID when generating new UUIDs
1580    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    /// Indicate that UUIDs should be created normally and not using a custom factory
1596    pub fn create_uuids_normally() {
1597        // Clear the sequence
1598        let mut seq_guard = UUID_SEQUENCE.lock().unwrap();
1599        *seq_guard = None;
1600    }
1601
1602    /// Generate a ULID
1603    pub fn ulid(time: Option<DateTime<Utc>>) -> Ulid {
1604        // Check for sequence first
1605        {
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(), // ULID library handles time internally
1622            None => Ulid::new(),
1623        }
1624    }
1625
1626    /// Indicate that ULIDs should be created normally and not using a custom factory
1627    pub fn create_ulids_normally() {
1628        let mut seq_guard = ULID_SEQUENCE.lock().unwrap();
1629        *seq_guard = None;
1630    }
1631
1632    /// Set the callable that will be used to generate ULIDs
1633    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    /// Set the sequence that will be used to generate ULIDs
1641    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    /// Always return the same ULID when generating new ULIDs
1647    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    /// Remove all strings from the casing caches
1663    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// Enums and structs for various options
1675
1676#[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}