Skip to main content

headless_lms_utils/
strings.rs

1use once_cell::sync::Lazy;
2use rand::{RngExt, distr::Alphanumeric, rng};
3use regex::Regex;
4
5static IETF_LANGUAGE_CODE_REGEX: Lazy<Regex> = Lazy::new(|| {
6    Regex::new(r"^[a-z]{2,3}(-[A-Z][a-z]{3})?(-[A-Z]{2})?$")
7        .expect("Invalid IETF language code regex.")
8});
9
10static HTML_TAG_REGEX: Lazy<Regex> =
11    Lazy::new(|| Regex::new(r"<[^>]*>").expect("Invalid HTML tag regex."));
12
13pub fn generate_random_string(length: usize) -> String {
14    rng()
15        .sample_iter(Alphanumeric)
16        .take(length)
17        .map(char::from)
18        .collect()
19}
20
21pub fn generate_easily_writable_random_string(length: usize) -> String {
22    rng()
23        .sample_iter(Alphanumeric)
24        .filter(|c: &u8| c.is_ascii_lowercase() || c.is_ascii_digit())
25        // Filter out characters that might be confused with each other
26        .filter(|c| c != &b'l' && c != &b'1' && c != &b'o' && c != &b'0')
27        .take(length)
28        .map(char::from)
29        .collect()
30}
31
32/// Checks whether the string is IETF language code where subtags are separated with underscore.
33pub fn is_ietf_language_code_like(string: &str) -> bool {
34    IETF_LANGUAGE_CODE_REGEX.is_match(string)
35}
36
37/// Removes all HTML tags from the input, leaving only the text content.
38pub fn strip_html_tags(input: &str) -> String {
39    HTML_TAG_REGEX.replace_all(input, "").into_owned()
40}
41
42/// Trims the string and returns `None` if the trimmed result is empty.
43pub fn non_empty_trimmed(s: &str) -> Option<&str> {
44    let trimmed = s.trim();
45    (!trimmed.is_empty()).then_some(trimmed)
46}
47
48/// Truncates UTF-8 text to a max byte length at a valid char boundary.
49pub fn truncate_utf8_at_boundary(s: &str, max_bytes: usize) -> &str {
50    if s.len() <= max_bytes {
51        return s;
52    }
53    let mut idx = max_bytes;
54    while idx > 0 && !s.is_char_boundary(idx) {
55        idx -= 1;
56    }
57    &s[..idx]
58}
59
60#[cfg(test)]
61mod test {
62    use super::*;
63
64    #[test]
65    fn ietf_language_code_validation_works() {
66        // Invalid scenarios
67        assert!(!is_ietf_language_code_like(""));
68        assert!(!is_ietf_language_code_like("en_us"));
69        assert!(!is_ietf_language_code_like("en_US"));
70        assert!(!is_ietf_language_code_like("in-cans"));
71        assert!(!is_ietf_language_code_like("in-cans-ca"));
72
73        // Valid scenarios
74        assert!(is_ietf_language_code_like("en"));
75        assert!(is_ietf_language_code_like("eng"));
76        assert!(is_ietf_language_code_like("en-US"));
77        assert!(is_ietf_language_code_like("in-Cans-CA"));
78    }
79
80    #[test]
81    fn non_empty_trimmed_trims_and_rejects_blank() {
82        assert_eq!(non_empty_trimmed("  hello  "), Some("hello"));
83        assert_eq!(non_empty_trimmed(""), None);
84        assert_eq!(non_empty_trimmed("   "), None);
85    }
86
87    #[test]
88    fn strip_html_tags_removes_all_tags() {
89        assert_eq!(
90            strip_html_tags("<em>Intro</em> to <strong>X</strong>"),
91            "Intro to X"
92        );
93        assert_eq!(strip_html_tags(r#"<a href="/x">link</a>"#), "link");
94        assert_eq!(strip_html_tags("plain text"), "plain text");
95        assert_eq!(strip_html_tags("<br>"), "");
96    }
97
98    #[test]
99    fn truncate_utf8_at_boundary_returns_original_when_short() {
100        let input = "heillä";
101        let result = truncate_utf8_at_boundary(input, 255);
102        assert_eq!(result, input);
103    }
104
105    #[test]
106    fn truncate_utf8_at_boundary_handles_finnish_characters() {
107        let input = format!("{}äz", "a".repeat(254));
108        let result = truncate_utf8_at_boundary(&input, 255);
109        assert_eq!(result.len(), 254);
110        assert!(result.is_char_boundary(result.len()));
111        assert_eq!(result, "a".repeat(254));
112    }
113
114    #[test]
115    fn truncate_utf8_at_boundary_handles_emoji() {
116        let input = format!("{}😀z", "a".repeat(254));
117        let result = truncate_utf8_at_boundary(&input, 255);
118        assert_eq!(result.len(), 254);
119        assert!(result.is_char_boundary(result.len()));
120        assert_eq!(result, "a".repeat(254));
121    }
122}