1use super::JsValue;
7use super::coerce::to_int32;
8
9pub fn call(this: &str, method: &str, args: &[JsValue]) -> Option<JsValue> {
15 match method {
16 "charAt" => char_at(this, args),
17 "charCodeAt" => char_code_at(this, args),
18 "codePointAt" => code_point_at(this, args),
19 "at" => at(this, args),
20 "indexOf" => index_of(this, args),
21 "lastIndexOf" => last_index_of(this, args),
22 "includes" => includes(this, args),
23 "startsWith" => starts_with(this, args),
24 "endsWith" => ends_with(this, args),
25 "slice" => slice(this, args),
26 "substring" => substring(this, args),
27 "substr" => substr(this, args),
28 "toUpperCase" | "toLocaleUpperCase" => Some(JsValue::String(this.to_uppercase())),
29 "toLowerCase" | "toLocaleLowerCase" => Some(JsValue::String(this.to_lowercase())),
30 "trim" => Some(JsValue::String(this.trim().to_string())),
31 "trimStart" | "trimLeft" => Some(JsValue::String(this.trim_start().to_string())),
32 "trimEnd" | "trimRight" => Some(JsValue::String(this.trim_end().to_string())),
33 "repeat" => repeat(this, args),
34 "padStart" => pad_start(this, args),
35 "padEnd" => pad_end(this, args),
36 "replace" => replace(this, args),
37 "concat" => concat(this, args),
38 "toString" | "valueOf" => Some(JsValue::String(this.to_string())),
39 _ => None,
40 }
41}
42
43pub fn property(this: &str, prop: &str) -> Option<JsValue> {
45 if prop == "length" {
46 let len: usize = this.chars().map(|c| if c as u32 > 0xFFFF { 2 } else { 1 }).sum();
47 return Some(JsValue::Number(len as f64));
48 }
49 if let Ok(idx) = prop.parse::<usize>() {
50 return this.chars().nth(idx).map(|c| JsValue::String(c.to_string()));
51 }
52 None
53}
54
55pub fn from_char_code(args: &[JsValue]) -> Option<JsValue> {
61 let mut result = String::new();
62 for arg in args {
63 let code = to_int32(arg) as u16;
64 result.push(char::from_u32(code as u32).unwrap_or('\u{FFFD}'));
65 }
66 Some(JsValue::String(result))
67}
68
69pub fn from_code_point(args: &[JsValue]) -> Option<JsValue> {
71 let mut result = String::new();
72 for arg in args {
73 let code = to_int32(arg);
74 if !(0..=0x10FFFF).contains(&code) { return None; }
75 result.push(char::from_u32(code as u32)?);
76 }
77 Some(JsValue::String(result))
78}
79
80fn char_at(this: &str, args: &[JsValue]) -> Option<JsValue> {
85 let idx = arg_int(args, 0, 0) as usize;
86 Some(JsValue::String(this.chars().nth(idx).map_or(String::new(), |c| c.to_string())))
87}
88
89fn char_code_at(this: &str, args: &[JsValue]) -> Option<JsValue> {
90 let idx = arg_int(args, 0, 0) as usize;
91 Some(this.chars().nth(idx).map_or(JsValue::Number(f64::NAN), |c| JsValue::Number(c as u32 as f64)))
92}
93
94fn code_point_at(this: &str, args: &[JsValue]) -> Option<JsValue> {
95 let idx = arg_int(args, 0, 0) as usize;
96 Some(this.chars().nth(idx).map_or(JsValue::Undefined, |c| JsValue::Number(c as u32 as f64)))
97}
98
99fn at(this: &str, args: &[JsValue]) -> Option<JsValue> {
100 let idx = arg_int(args, 0, 0);
101 let len = this.chars().count() as i32;
102 let resolved = if idx < 0 { len + idx } else { idx };
103 if resolved < 0 || resolved >= len {
104 return Some(JsValue::Undefined);
105 }
106 this.chars().nth(resolved as usize).map(|c| JsValue::String(c.to_string()))
107}
108
109fn index_of(this: &str, args: &[JsValue]) -> Option<JsValue> {
110 let search = arg_str(args, 0)?;
111 let from = arg_int(args, 1, 0).max(0) as usize;
112 if from > this.len() {
113 return Some(JsValue::Number(-1.0));
114 }
115 Some(JsValue::Number(this[from..].find(&*search).map_or(-1.0, |p| (p + from) as f64)))
116}
117
118fn last_index_of(this: &str, args: &[JsValue]) -> Option<JsValue> {
119 let search = arg_str(args, 0)?;
120 Some(JsValue::Number(this.rfind(&*search).map_or(-1.0, |p| p as f64)))
121}
122
123fn includes(this: &str, args: &[JsValue]) -> Option<JsValue> {
124 let search = arg_str(args, 0)?;
125 let from = arg_int(args, 1, 0).max(0) as usize;
126 if from > this.len() {
127 return Some(JsValue::Boolean(false));
128 }
129 Some(JsValue::Boolean(this[from..].contains(&*search)))
130}
131
132fn starts_with(this: &str, args: &[JsValue]) -> Option<JsValue> {
133 let search = arg_str(args, 0)?;
134 let pos = arg_int(args, 1, 0).max(0) as usize;
135 if pos > this.len() { return Some(JsValue::Boolean(false)); }
136 Some(JsValue::Boolean(this[pos..].starts_with(&*search)))
137}
138
139fn ends_with(this: &str, args: &[JsValue]) -> Option<JsValue> {
140 let search = arg_str(args, 0)?;
141 Some(JsValue::Boolean(this.ends_with(&*search)))
142}
143
144fn slice(this: &str, args: &[JsValue]) -> Option<JsValue> {
145 let len = this.chars().count() as i32;
146 let start = resolve_index(arg_int(args, 0, 0), len);
147 let end = resolve_index(arg_int(args, 1, len), len);
148 if start >= end { return Some(JsValue::String(String::new())); }
149 Some(JsValue::String(this.chars().skip(start as usize).take((end - start) as usize).collect()))
150}
151
152fn substring(this: &str, args: &[JsValue]) -> Option<JsValue> {
153 let len = this.chars().count() as i32;
154 let mut start = arg_int(args, 0, 0).clamp(0, len);
155 let mut end = arg_int(args, 1, len).clamp(0, len);
156 if start > end { std::mem::swap(&mut start, &mut end); }
157 Some(JsValue::String(this.chars().skip(start as usize).take((end - start) as usize).collect()))
158}
159
160fn substr(this: &str, args: &[JsValue]) -> Option<JsValue> {
161 let len = this.chars().count() as i32;
162 let start = resolve_index(arg_int(args, 0, 0), len);
163 let count = arg_int(args, 1, len).max(0);
164 if start >= len || count <= 0 { return Some(JsValue::String(String::new())); }
165 Some(JsValue::String(this.chars().skip(start as usize).take(count as usize).collect()))
166}
167
168fn repeat(this: &str, args: &[JsValue]) -> Option<JsValue> {
169 let count = arg_int(args, 0, 0);
170 if !(0..=10_000).contains(&count) { return None; }
171 Some(JsValue::String(this.repeat(count as usize)))
172}
173
174fn pad_start(this: &str, args: &[JsValue]) -> Option<JsValue> {
175 let target_len = arg_int(args, 0, 0) as usize;
176 let fill = arg_str(args, 1).unwrap_or_else(|| " ".to_string());
177 if this.len() >= target_len || fill.is_empty() { return Some(JsValue::String(this.to_string())); }
178 let padding: String = fill.chars().cycle().take(target_len - this.len()).collect();
179 Some(JsValue::String(format!("{padding}{this}")))
180}
181
182fn pad_end(this: &str, args: &[JsValue]) -> Option<JsValue> {
183 let target_len = arg_int(args, 0, 0) as usize;
184 let fill = arg_str(args, 1).unwrap_or_else(|| " ".to_string());
185 if this.len() >= target_len || fill.is_empty() { return Some(JsValue::String(this.to_string())); }
186 let padding: String = fill.chars().cycle().take(target_len - this.len()).collect();
187 Some(JsValue::String(format!("{this}{padding}")))
188}
189
190fn replace(this: &str, args: &[JsValue]) -> Option<JsValue> {
191 let search = arg_str(args, 0)?;
192 let replacement = arg_str(args, 1)?;
193 Some(JsValue::String(this.replacen(&*search, &replacement, 1)))
194}
195
196fn concat(this: &str, args: &[JsValue]) -> Option<JsValue> {
197 let mut result = this.to_string();
198 for arg in args {
199 result.push_str(&super::coerce::to_string(arg));
200 }
201 Some(JsValue::String(result))
202}
203
204fn arg_int(args: &[JsValue], index: usize, default: i32) -> i32 {
209 args.get(index).map_or(default, super::coerce::to_int32)
210}
211
212fn arg_str(args: &[JsValue], index: usize) -> Option<String> {
213 args.get(index).map(super::coerce::to_string)
214}
215
216fn resolve_index(idx: i32, len: i32) -> i32 {
217 if idx < 0 { (len + idx).max(0) } else { idx.min(len) }
218}
219
220#[cfg(test)]
225mod tests {
226 use super::*;
227
228 fn s(v: &str) -> JsValue { JsValue::String(v.into()) }
229 fn n(v: f64) -> JsValue { JsValue::Number(v) }
230
231 #[test]
232 fn test_char_at() {
233 assert_eq!(call("abc", "charAt", &[n(0.0)]), Some(s("a")));
234 assert_eq!(call("abc", "charAt", &[n(5.0)]), Some(s("")));
235 }
236
237 #[test]
238 fn test_char_code_at() {
239 assert_eq!(call("A", "charCodeAt", &[n(0.0)]), Some(n(65.0)));
240 }
241
242 #[test]
243 fn test_index_of() {
244 assert_eq!(call("hello world", "indexOf", &[s("world")]), Some(n(6.0)));
245 assert_eq!(call("hello", "indexOf", &[s("x")]), Some(n(-1.0)));
246 }
247
248 #[test]
249 fn test_includes() {
250 assert_eq!(call("hello", "includes", &[s("ell")]), Some(JsValue::Boolean(true)));
251 assert_eq!(call("hello", "includes", &[s("xyz")]), Some(JsValue::Boolean(false)));
252 }
253
254 #[test]
255 fn test_slice() {
256 assert_eq!(call("hello", "slice", &[n(1.0), n(3.0)]), Some(s("el")));
257 assert_eq!(call("hello", "slice", &[n(-3.0)]), Some(s("llo")));
258 }
259
260 #[test]
261 fn test_case() {
262 assert_eq!(call("Hello", "toUpperCase", &[]), Some(s("HELLO")));
263 assert_eq!(call("Hello", "toLowerCase", &[]), Some(s("hello")));
264 }
265
266 #[test]
267 fn test_trim() {
268 assert_eq!(call(" hi ", "trim", &[]), Some(s("hi")));
269 }
270
271 #[test]
272 fn test_repeat() {
273 assert_eq!(call("ab", "repeat", &[n(3.0)]), Some(s("ababab")));
274 }
275
276 #[test]
277 fn test_pad() {
278 assert_eq!(call("5", "padStart", &[n(3.0), s("0")]), Some(s("005")));
279 assert_eq!(call("5", "padEnd", &[n(3.0), s("0")]), Some(s("500")));
280 }
281
282 #[test]
283 fn test_replace() {
284 assert_eq!(call("aabbcc", "replace", &[s("bb"), s("XX")]), Some(s("aaXXcc")));
285 assert_eq!(call("abab", "replace", &[s("ab"), s("X")]), Some(s("Xab")));
286 }
287
288 #[test]
289 fn test_from_char_code() {
290 assert_eq!(from_char_code(&[n(72.0), n(101.0), n(108.0), n(108.0), n(111.0)]), Some(s("Hello")));
291 }
292
293 #[test]
294 fn test_at() {
295 assert_eq!(call("abc", "at", &[n(0.0)]), Some(s("a")));
296 assert_eq!(call("abc", "at", &[n(-1.0)]), Some(s("c")));
297 }
298
299 #[test]
300 fn test_property_length() {
301 assert_eq!(property("hello", "length"), Some(n(5.0)));
302 }
303
304 #[test]
305 fn test_bracket_access() {
306 assert_eq!(property("abc", "0"), Some(s("a")));
307 assert_eq!(property("abc", "2"), Some(s("c")));
308 }
309}