Skip to main content

windjammer_runtime/
strings.rs

1//! String utilities — Windjammer `std::strings` maps here.
2
3/// Convert to uppercase
4pub fn to_upper<S: AsRef<str>>(s: S) -> String {
5    s.as_ref().to_uppercase()
6}
7
8/// Convert to lowercase
9pub fn to_lower<S: AsRef<str>>(s: S) -> String {
10    s.as_ref().to_lowercase()
11}
12
13/// Trim whitespace
14pub fn trim<S: AsRef<str>>(s: S) -> String {
15    s.as_ref().trim().to_string()
16}
17
18/// Trim whitespace from start
19pub fn trim_start<S: AsRef<str>>(s: S) -> String {
20    s.as_ref().trim_start().to_string()
21}
22
23/// Trim whitespace from end
24pub fn trim_end<S: AsRef<str>>(s: S) -> String {
25    s.as_ref().trim_end().to_string()
26}
27
28/// Split string by delimiter
29pub fn split<S: AsRef<str>>(s: S, delimiter: &str) -> Vec<String> {
30    s.as_ref().split(delimiter).map(|s| s.to_string()).collect()
31}
32
33/// Split text into lines (`\n` / `\r\n` — no `.as_bytes()` needed in Windjammer).
34pub fn split_lines<S: AsRef<str>>(text: S) -> Vec<String> {
35    let mut lines: Vec<String> = Vec::new();
36    let mut current = String::new();
37    for ch in text.as_ref().chars() {
38        if ch == '\n' {
39            lines.push(current);
40            current = String::new();
41        } else if ch != '\r' {
42            current.push(ch);
43        }
44    }
45    if !current.is_empty() || text.as_ref().ends_with('\n') {
46        lines.push(current);
47    }
48    lines
49}
50
51/// Split on ASCII whitespace (space / tab).
52pub fn split_whitespace<S: AsRef<str>>(line: S) -> Vec<String> {
53    let mut parts: Vec<String> = Vec::new();
54    let mut current = String::new();
55    let mut in_word = false;
56    for ch in line.as_ref().chars() {
57        if ch == ' ' || ch == '\t' {
58            if in_word {
59                parts.push(current);
60                current = String::new();
61                in_word = false;
62            }
63        } else {
64            current.push(ch);
65            in_word = true;
66        }
67    }
68    if in_word {
69        parts.push(current);
70    }
71    parts
72}
73
74/// Parse signed integer (returns 0 on failure — matches legacy scene parser).
75pub fn parse_i32<S: AsRef<str>>(s: S) -> i32 {
76    s.as_ref().trim().parse().unwrap_or(0)
77}
78
79/// Parse float (returns 0.0 on failure).
80pub fn parse_f32<S: AsRef<str>>(s: S) -> f32 {
81    s.as_ref().trim().parse().unwrap_or(0.0)
82}
83
84/// Parse bool from `"true"` / `"false"`.
85pub fn parse_bool<S: AsRef<str>>(s: S) -> bool {
86    s.as_ref().trim() == "true"
87}
88
89/// ASCII byte at index (for line-oriented parsers on `.wjscene` files).
90pub fn byte_at<S: AsRef<str>>(s: S, index: usize) -> u8 {
91    s.as_ref().as_bytes().get(index).copied().unwrap_or(0)
92}
93
94/// Join strings with delimiter
95pub fn join(parts: &[String], delimiter: &str) -> String {
96    parts.join(delimiter)
97}
98
99/// Unicode characters in a string (for editor widgets, parsers, etc.).
100pub fn chars<S: AsRef<str>>(s: S) -> Vec<char> {
101    s.as_ref().chars().collect()
102}
103
104/// Build a string from character codepoints.
105pub fn from_chars(parts: &[char]) -> String {
106    parts.iter().collect()
107}
108
109/// Substring by character indices `[start, end)`.
110pub fn substring_chars<S: AsRef<str>>(s: S, start: usize, end: usize) -> String {
111    s.as_ref()
112        .chars()
113        .skip(start)
114        .take(end.saturating_sub(start))
115        .collect()
116}
117
118/// Check if string contains substring
119pub fn contains<S: AsRef<str>>(s: S, substring: &str) -> bool {
120    s.as_ref().contains(substring)
121}
122
123/// Check if string starts with prefix
124pub fn starts_with<S: AsRef<str>>(s: S, prefix: &str) -> bool {
125    s.as_ref().starts_with(prefix)
126}
127
128/// Check if string ends with suffix
129pub fn ends_with<S: AsRef<str>>(s: S, suffix: &str) -> bool {
130    s.as_ref().ends_with(suffix)
131}
132
133/// Replace all occurrences
134pub fn replace<S: AsRef<str>>(s: S, from: &str, to: &str) -> String {
135    s.as_ref().replace(from, to)
136}
137
138/// Get substring from start to end index (exclusive)
139pub fn substring<S: AsRef<str>>(s: S, start: usize, end: usize) -> String {
140    s.as_ref()
141        .chars()
142        .skip(start)
143        .take(end.saturating_sub(start))
144        .collect()
145}
146
147/// Get string length in bytes
148pub fn len<S: AsRef<str>>(s: S) -> usize {
149    s.as_ref().len()
150}
151
152/// Check if string is empty
153pub fn is_empty<S: AsRef<str>>(s: S) -> bool {
154    s.as_ref().is_empty()
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn test_case_conversion() {
163        assert_eq!(to_upper("hello"), "HELLO");
164        assert_eq!(to_lower("WORLD"), "world");
165    }
166
167    #[test]
168    fn test_trim() {
169        assert_eq!(trim("  hello  "), "hello");
170    }
171
172    #[test]
173    fn test_split_join() {
174        let parts = split("a,b,c", ",");
175        assert_eq!(parts, vec!["a", "b", "c"]);
176        assert_eq!(join(&parts, "-"), "a-b-c");
177    }
178
179    #[test]
180    fn test_split_lines() {
181        let lines = split_lines("a\nb\r\nc\n");
182        assert_eq!(lines, vec!["a", "b", "c", ""]);
183    }
184
185    #[test]
186    fn test_split_whitespace() {
187        let parts = split_whitespace("  hello\tworld  ");
188        assert_eq!(parts, vec!["hello", "world"]);
189    }
190
191    #[test]
192    fn test_parse_i32() {
193        assert_eq!(parse_i32("-42"), -42);
194        assert_eq!(parse_i32("  7 "), 7);
195        assert_eq!(parse_i32("bad"), 0);
196    }
197
198    #[test]
199    fn test_parse_f32() {
200        assert!((parse_f32("3.14") - 3.14).abs() < 0.001);
201        assert!((parse_f32("-2.5") - (-2.5)).abs() < 0.001);
202    }
203
204    #[test]
205    fn test_parse_bool() {
206        assert!(parse_bool("true"));
207        assert!(!parse_bool("false"));
208    }
209
210    #[test]
211    fn test_byte_at() {
212        assert_eq!(byte_at("ABC", 0), b'A');
213        assert_eq!(byte_at("ABC", 2), b'C');
214        assert_eq!(byte_at("ABC", 99), 0);
215    }
216
217    #[test]
218    fn test_chars_and_from_chars() {
219        let c = chars("hello");
220        assert_eq!(c, vec!['h', 'e', 'l', 'l', 'o']);
221        assert_eq!(from_chars(&c), "hello");
222        assert_eq!(substring_chars("hello", 1, 4), "ell");
223    }
224}