illuminate_string/
lib.rs

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; // <-- The path changes here
8use std::sync::{Arc, Mutex, OnceLock};
9use textwrap;
10use ulid::Ulid;
11use unicode_segmentation::UnicodeSegmentation;
12use unidecode::unidecode;
13use uuid::Uuid;
14
15// Global caches for performance
16static 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
23// Factory functions
24static 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
29// Sequence factories
30static 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
37// Invisible characters constant
38const 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
40// Pluralization rules (simplified English rules)
41static 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    /// Return the remainder of a string after the first occurrence of a given value
117    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    /// Return the remainder of a string after the last occurrence of a given value
130    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    /// Transliterate a UTF-8 value to ASCII
143    pub fn ascii(value: &str) -> String {
144        unidecode(value)
145    }
146
147    /// Transliterate a string to its closest ASCII representation
148    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    /// Get the portion of a string before the first occurrence of a given value
172    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    /// Get the portion of a string before the last occurrence of a given value
185    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    /// Get the portion of a string between two given values
198    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    /// Get the smallest possible portion of a string between two given values
208    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    /// Convert a value to camel case
218    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    /// Get the character at the specified index
234    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    /// Remove the given string(s) if it exists at the start of the haystack
254    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    /// Remove the given string(s) if it exists at the end of the haystack
264    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    /// Determine if a given string contains a given substring
274    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    /// Determine if a given string contains all array values
300    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    /// Determine if a given string doesn't contain a given substring
310    pub fn doesnt_contain(haystack: &str, needles: &[&str], ignore_case: bool) -> bool {
311        !Self::contains(haystack, needles, ignore_case)
312    }
313
314    /// Convert the case of a string
315    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(), // Case folding approximation
321        }
322    }
323
324    /// Replace consecutive instances of a given character with a single character
325    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    /// Determine if a given string ends with a given substring
338    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    /// Determine if a given string doesn't end with a given substring
348    pub fn doesnt_end_with(haystack: &str, needles: &[&str]) -> bool {
349        !Self::ends_with(haystack, needles)
350    }
351
352    /// Extracts an excerpt from text that matches the first instance of a phrase
353    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    /// Cap a string with a single instance of a given value
387    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    /// Wrap the string with the given strings
395    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    /// Unwrap the string with the given strings
401    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    /// Determine if a given string matches a given pattern (with wildcards)
417    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    /// Determine if a given string is 7 bit ASCII
442    pub fn is_ascii(value: &str) -> bool {
443        value.is_ascii()
444    }
445
446    /// Determine if a given value is valid JSON
447    pub fn is_json(value: &str) -> bool {
448        serde_json::from_str::<serde_json::Value>(value).is_ok()
449    }
450
451    /// Determine if a given value is a valid URL
452    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    /// Determine if a given value is a valid UUID
465    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    /// Determine if a given value is a valid ULID
482    pub fn is_ulid(value: &str) -> bool {
483        Ulid::from_string(value).is_ok()
484    }
485
486    /// Convert a string to kebab case
487    pub fn kebab(value: &str) -> String {
488        Self::snake(value, "-")
489    }
490
491    /// Return the length of the given string
492    pub fn length(value: &str) -> usize {
493        value.graphemes(true).count()
494    }
495
496    /// Limit the number of characters in a string
497    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        // For word preservation, we need to work with the actual string
510        let truncated: String = graphemes.iter().take(limit).copied().collect();
511        let trimmed = truncated.trim_end();
512
513        // Find the last space and truncate there
514        if let Some(last_space) = trimmed.rfind(' ') {
515            format!("{}{}", &trimmed[..last_space], end)
516        } else {
517            format!("{}{}", trimmed, end)
518        }
519    }
520
521    /// Convert the given string to lower-case
522    pub fn lower(value: &str) -> String {
523        value.to_lowercase()
524    }
525
526    /// Limit the number of words in a string
527    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    /// Converts GitHub flavored Markdown into HTML
539    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    /// Converts inline Markdown into HTML
561    pub fn inline_markdown(string: &str, options: MarkdownOptions) -> String {
562        // For inline markdown, we strip block elements
563        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    /// Masks a portion of a string with a repeated character
569    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    /// Get the string matching the given pattern
601    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    /// Determine if a given string matches a given pattern
620    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    /// Get all strings matching the given pattern
632    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    /// Remove all non-numeric characters from a string
643    pub fn numbers(value: &str) -> String {
644        value.chars().filter(|c| c.is_numeric()).collect()
645    }
646
647    /// Pad both sides of a string with another
648    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    /// Pad the left side of a string with another
670    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    /// Pad the right side of a string with another
687    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    /// Parse a Class[@]method style callback into class and method
704    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    /// Get the plural form of an English word
723    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) // fallback
737        };
738
739        if prepend_count {
740            format!("{} {}", count, plural_form)
741        } else {
742            plural_form
743        }
744    }
745
746    /// Pluralize the last word of an English, studly caps case string
747    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    /// Pluralize the last word of an English, Pascal caps case string
762    pub fn plural_pascal(value: &str, count: i32) -> String {
763        Self::plural_studly(value, count)
764    }
765
766    /// Generate a random, secure password
767    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    /// Find the multi-byte safe position of the first occurrence of a given substring
811    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    /// Generate a more truly "random" alpha-numeric string
827    pub fn random(length: usize) -> String {
828        // Check for sequence first
829        {
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    /// Set the callable that will be used to generate random strings
856    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    /// Set the sequence that will be used to generate random strings
864    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    /// Indicate that random strings should be created normally
870    pub fn create_random_strings_normally() {
871        // Clear the factory and sequence
872        // Note: OnceLock doesn't have a clear method, so we'd need to restructure
873        // For now, we'll clear the sequence
874        let mut seq_guard = RANDOM_SEQUENCE.lock().unwrap();
875        *seq_guard = None;
876    }
877
878    /// Repeat the given string
879    pub fn repeat(string: &str, times: usize) -> String {
880        string.repeat(times)
881    }
882
883    /// Replace a given value in the string sequentially with an array
884    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    /// Replace the given value in the given string
898    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        // Use a unique placeholder to avoid double replacement
907        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        // Replace placeholders with actual replacements
922        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    /// Replace the first occurrence of a given value in the string
931    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    /// Replace the first occurrence of the given value if it appears at the start
948    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    /// Replace the last occurrence of a given value in the string
961    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    /// Replace the last occurrence of a given value if it appears at the end
978    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    /// Replace the patterns matching the given regular expression
991    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    /// Remove any occurrence of the given string in the subject
1008    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    /// Reverse the given string
1014    pub fn reverse(value: &str) -> String {
1015        value.chars().rev().collect()
1016    }
1017
1018    /// Begin a string with a single instance of a given value
1019    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    /// Convert the given string to upper-case
1027    pub fn upper(value: &str) -> String {
1028        value.to_uppercase()
1029    }
1030
1031    /// Convert the given string to title case
1032    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    /// Convert the given string to headline case
1041    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    /// Convert the given string to APA-style title case
1065    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    /// Get the singular form of an English word
1112    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    /// Generate a URL friendly "slug" from a given string
1122    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        // Convert all dashes/underscores into separator
1135        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        // Replace dictionary words
1141        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            // Default dictionary
1148            let replacement = format!("{}at{}", separator, separator);
1149            title = title.replace("@", &replacement);
1150        }
1151
1152        // Remove all characters that are not the separator, letters, numbers, or whitespace
1153        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        // Replace all separator characters and whitespace by a single separator
1158        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    /// Convert a string to snake case
1168    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        // Replace spaces with delimiter, then handle camelCase
1186        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: &regex::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    /// Remove all whitespace from both ends of a string
1204    pub fn trim(value: &str) -> String {
1205        Self::trim_custom(value, None)
1206    }
1207
1208    /// Remove all whitespace from both ends of a string with custom character list
1209    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                // Trim from start
1217                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                // Trim from end
1226                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    /// Remove all whitespace from the beginning of a string
1242    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    /// Remove all whitespace from the end of a string
1263    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    /// Remove all "extra" blank space from the given string
1286    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    /// Determine if a given string starts with a given substring
1293    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    /// Determine if a given string doesn't start with a given substring
1303    pub fn doesnt_start_with(haystack: &str, needles: &[&str]) -> bool {
1304        !Self::starts_with(haystack, needles)
1305    }
1306
1307    /// Convert a value to studly caps case
1308    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    /// Convert a value to Pascal case (alias for studly)
1325    pub fn pascal(value: &str) -> String {
1326        Self::studly(value)
1327    }
1328
1329    /// Returns the portion of the string specified by the start and length parameters
1330    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    /// Returns the number of substring occurrences
1361    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    /// Replace text within a portion of a string
1389    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    /// Swap multiple keywords in a string with other keywords
1421    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    /// Take the first or last {$limit} characters of a string
1430    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    /// Convert the given string to Base64 encoding
1439    pub fn to_base64(string: &str) -> String {
1440        general_purpose::STANDARD.encode(string.as_bytes())
1441    }
1442
1443    /// Decode the given Base64 encoded string
1444    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    /// Make a string's first character lowercase
1456    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    /// Make a string's first character uppercase
1465    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    /// Split a string into pieces by uppercase characters
1474    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    /// Get the number of words a string contains
1483    pub fn word_count(string: &str, characters: Option<&str>) -> usize {
1484        if let Some(chars) = characters {
1485            // Custom word character definition
1486            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    /// Wrap a string to a given number of characters
1495    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    /// Generate a UUID (version 4)
1511    pub fn uuid() -> Uuid {
1512        // Check for sequence first
1513        {
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    /// Generate a UUID (version 7)
1531    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(), // Simplified - would need uuid v7 implementation
1538            None => Uuid::new_v4(),
1539        }
1540    }
1541
1542    /// Generate a time-ordered UUID
1543    pub fn ordered_uuid() -> Uuid {
1544        if let Some(factory) = UUID_FACTORY.get() {
1545            return factory();
1546        }
1547
1548        // Simplified implementation - in reality would use COMB UUID
1549        Uuid::new_v4()
1550    }
1551
1552    /// Set the callable that will be used to generate UUIDs
1553    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    /// Set the sequence that will be used to generate UUIDs
1561    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    /// Always return the same UUID when generating new UUIDs
1567    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    /// Indicate that UUIDs should be created normally and not using a custom factory
1583    pub fn create_uuids_normally() {
1584        // Clear the sequence
1585        let mut seq_guard = UUID_SEQUENCE.lock().unwrap();
1586        *seq_guard = None;
1587    }
1588
1589    /// Generate a ULID
1590    pub fn ulid(time: Option<DateTime<Utc>>) -> Ulid {
1591        // Check for sequence first
1592        {
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(), // ULID library handles time internally
1609            None => Ulid::new(),
1610        }
1611    }
1612
1613    /// Indicate that ULIDs should be created normally and not using a custom factory
1614    pub fn create_ulids_normally() {
1615        let mut seq_guard = ULID_SEQUENCE.lock().unwrap();
1616        *seq_guard = None;
1617    }
1618
1619    /// Set the callable that will be used to generate ULIDs
1620    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    /// Set the sequence that will be used to generate ULIDs
1628    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    /// Always return the same ULID when generating new ULIDs
1634    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    /// Remove all strings from the casing caches
1650    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// Enums and structs for various options
1662
1663#[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}