headless_lms_utils/
strings.rs1use 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(|c| c != &b'l' && c != &b'1' && c != &b'o' && c != &b'0')
27 .take(length)
28 .map(char::from)
29 .collect()
30}
31
32pub fn is_ietf_language_code_like(string: &str) -> bool {
34 IETF_LANGUAGE_CODE_REGEX.is_match(string)
35}
36
37pub fn strip_html_tags(input: &str) -> String {
39 HTML_TAG_REGEX.replace_all(input, "").into_owned()
40}
41
42pub fn truncate_utf8_at_boundary(s: &str, max_bytes: usize) -> &str {
44 if s.len() <= max_bytes {
45 return s;
46 }
47 let mut idx = max_bytes;
48 while idx > 0 && !s.is_char_boundary(idx) {
49 idx -= 1;
50 }
51 &s[..idx]
52}
53
54#[cfg(test)]
55mod test {
56 use super::*;
57
58 #[test]
59 fn ietf_language_code_validation_works() {
60 assert!(!is_ietf_language_code_like(""));
62 assert!(!is_ietf_language_code_like("en_us"));
63 assert!(!is_ietf_language_code_like("en_US"));
64 assert!(!is_ietf_language_code_like("in-cans"));
65 assert!(!is_ietf_language_code_like("in-cans-ca"));
66
67 assert!(is_ietf_language_code_like("en"));
69 assert!(is_ietf_language_code_like("eng"));
70 assert!(is_ietf_language_code_like("en-US"));
71 assert!(is_ietf_language_code_like("in-Cans-CA"));
72 }
73
74 #[test]
75 fn strip_html_tags_removes_all_tags() {
76 assert_eq!(
77 strip_html_tags("<em>Intro</em> to <strong>X</strong>"),
78 "Intro to X"
79 );
80 assert_eq!(strip_html_tags(r#"<a href="/x">link</a>"#), "link");
81 assert_eq!(strip_html_tags("plain text"), "plain text");
82 assert_eq!(strip_html_tags("<br>"), "");
83 }
84
85 #[test]
86 fn truncate_utf8_at_boundary_returns_original_when_short() {
87 let input = "heillä";
88 let result = truncate_utf8_at_boundary(input, 255);
89 assert_eq!(result, input);
90 }
91
92 #[test]
93 fn truncate_utf8_at_boundary_handles_finnish_characters() {
94 let input = format!("{}äz", "a".repeat(254));
95 let result = truncate_utf8_at_boundary(&input, 255);
96 assert_eq!(result.as_bytes().len(), 254);
97 assert!(result.is_char_boundary(result.len()));
98 assert_eq!(result, "a".repeat(254));
99 }
100
101 #[test]
102 fn truncate_utf8_at_boundary_handles_emoji() {
103 let input = format!("{}😀z", "a".repeat(254));
104 let result = truncate_utf8_at_boundary(&input, 255);
105 assert_eq!(result.as_bytes().len(), 254);
106 assert!(result.is_char_boundary(result.len()));
107 assert_eq!(result, "a".repeat(254));
108 }
109}