Skip to main content

pipa/builtins/
string.rs

1use crate::host::HostFunction;
2use crate::object::object::JSObject;
3use crate::runtime::context::JSContext;
4use crate::value::JSValue;
5
6fn create_builtin_function(ctx: &mut JSContext, name: &str) -> JSValue {
7    let mut func = crate::object::function::JSFunction::new_builtin(ctx.intern(name), 1);
8    func.set_builtin_marker(ctx, name);
9    let ptr = Box::into_raw(Box::new(func)) as usize;
10    ctx.runtime_mut().gc_heap_mut().track_function(ptr);
11    JSValue::new_function(ptr)
12}
13
14pub fn init_string(ctx: &mut JSContext) {
15    let string_atom = ctx.common_atoms.string;
16
17    let mut string_func = crate::object::function::JSFunction::new_builtin(string_atom, 1);
18    string_func.set_builtin_marker(ctx, "string_constructor");
19
20    string_func.base.set(
21        ctx.intern("fromCharCode"),
22        create_builtin_function(ctx, "string_fromCharCode"),
23    );
24    string_func.base.set(
25        ctx.intern("fromCodePoint"),
26        create_builtin_function(ctx, "string_fromCodePoint"),
27    );
28
29    let string_ptr = Box::into_raw(Box::new(string_func)) as usize;
30    ctx.runtime_mut().gc_heap_mut().track_function(string_ptr);
31    let string_value = JSValue::new_function(string_ptr);
32    let global = ctx.global();
33    if global.is_object() {
34        let global_obj = global.as_object_mut();
35        global_obj.set(string_atom, string_value);
36    }
37
38    let proto_atom = ctx.intern("StringPrototype");
39    let mut proto_obj = JSObject::new();
40    proto_obj.set(
41        ctx.intern("charAt"),
42        create_builtin_function(ctx, "string_charAt"),
43    );
44    proto_obj.set(
45        ctx.intern("charCodeAt"),
46        create_builtin_function(ctx, "string_charCodeAt"),
47    );
48    proto_obj.set(
49        ctx.intern("concat"),
50        create_builtin_function(ctx, "string_concat"),
51    );
52    proto_obj.set(
53        ctx.intern("indexOf"),
54        create_builtin_function(ctx, "string_indexOf"),
55    );
56    proto_obj.set(
57        ctx.intern("lastIndexOf"),
58        create_builtin_function(ctx, "string_lastIndexOf"),
59    );
60    proto_obj.set(
61        ctx.intern("slice"),
62        create_builtin_function(ctx, "string_slice"),
63    );
64    proto_obj.set(
65        ctx.intern("substring"),
66        create_builtin_function(ctx, "string_substring"),
67    );
68    proto_obj.set(
69        ctx.intern("toString"),
70        create_builtin_function(ctx, "string_toString"),
71    );
72    proto_obj.set(
73        ctx.intern("valueOf"),
74        create_builtin_function(ctx, "string_toString"),
75    );
76    proto_obj.set(
77        ctx.intern("toLowerCase"),
78        create_builtin_function(ctx, "string_toLowerCase"),
79    );
80    proto_obj.set(
81        ctx.intern("toUpperCase"),
82        create_builtin_function(ctx, "string_toUpperCase"),
83    );
84    proto_obj.set(
85        ctx.intern("split"),
86        create_builtin_function(ctx, "string_split"),
87    );
88    proto_obj.set(
89        ctx.common_atoms.length,
90        create_builtin_function(ctx, "string_length"),
91    );
92    proto_obj.set(
93        ctx.intern("trim"),
94        create_builtin_function(ctx, "string_trim"),
95    );
96    proto_obj.set(
97        ctx.intern("trimStart"),
98        create_builtin_function(ctx, "string_trimStart"),
99    );
100    proto_obj.set(
101        ctx.intern("trimLeft"),
102        create_builtin_function(ctx, "string_trimStart"),
103    );
104    proto_obj.set(
105        ctx.intern("trimEnd"),
106        create_builtin_function(ctx, "string_trimEnd"),
107    );
108    proto_obj.set(
109        ctx.intern("trimRight"),
110        create_builtin_function(ctx, "string_trimEnd"),
111    );
112    proto_obj.set(
113        ctx.intern("startsWith"),
114        create_builtin_function(ctx, "string_startsWith"),
115    );
116    proto_obj.set(
117        ctx.intern("endsWith"),
118        create_builtin_function(ctx, "string_endsWith"),
119    );
120    proto_obj.set(
121        ctx.intern("repeat"),
122        create_builtin_function(ctx, "string_repeat"),
123    );
124    proto_obj.set(
125        ctx.intern("includes"),
126        create_builtin_function(ctx, "string_includes"),
127    );
128    proto_obj.set(
129        ctx.intern("replace"),
130        create_builtin_function(ctx, "string_replace"),
131    );
132    proto_obj.set(
133        ctx.intern("padStart"),
134        create_builtin_function(ctx, "string_padStart"),
135    );
136    proto_obj.set(
137        ctx.intern("padEnd"),
138        create_builtin_function(ctx, "string_padEnd"),
139    );
140    proto_obj.set(
141        ctx.intern("replaceAll"),
142        create_builtin_function(ctx, "string_replaceAll"),
143    );
144    proto_obj.set(
145        ctx.intern("search"),
146        create_builtin_function(ctx, "string_search"),
147    );
148    proto_obj.set(
149        ctx.intern("match"),
150        create_builtin_function(ctx, "string_match"),
151    );
152    proto_obj.set(ctx.intern("at"), create_builtin_function(ctx, "string_at"));
153    proto_obj.set(
154        ctx.intern("isWellFormed"),
155        create_builtin_function(ctx, "string_isWellFormed"),
156    );
157    proto_obj.set(
158        ctx.intern("toWellFormed"),
159        create_builtin_function(ctx, "string_toWellFormed"),
160    );
161    proto_obj.set(
162        ctx.intern("codePointAt"),
163        create_builtin_function(ctx, "string_codePointAt"),
164    );
165    proto_obj.set(
166        ctx.intern("matchAll"),
167        create_builtin_function(ctx, "string_matchAll"),
168    );
169    proto_obj.set(
170        ctx.intern("substr"),
171        create_builtin_function(ctx, "string_substr"),
172    );
173
174    if let Some(obj_proto_ptr) = ctx.get_object_prototype() {
175        proto_obj.prototype = Some(obj_proto_ptr);
176    }
177
178    let proto_ptr = Box::into_raw(Box::new(proto_obj)) as usize;
179    ctx.runtime_mut().gc_heap_mut().track(proto_ptr);
180
181    ctx.set_string_prototype(proto_ptr);
182    let proto_value = JSValue::new_object(proto_ptr);
183
184    let string_func_ptr = string_ptr as *mut crate::object::function::JSFunction;
185    unsafe {
186        (*string_func_ptr)
187            .base
188            .set(ctx.common_atoms.prototype, proto_value);
189    }
190
191    if global.is_object() {
192        let global_obj = global.as_object_mut();
193        global_obj.set(proto_atom, proto_value);
194    }
195}
196
197pub fn register_builtins(ctx: &mut JSContext) {
198    ctx.register_builtin(
199        "string_constructor",
200        HostFunction::new("String", 1, string_constructor),
201    );
202    ctx.register_builtin(
203        "string_charAt",
204        HostFunction::new("charAt", 1, string_char_at),
205    );
206    ctx.register_builtin(
207        "string_charCodeAt",
208        HostFunction::new("charCodeAt", 1, string_char_code_at),
209    );
210    ctx.register_builtin(
211        "string_concat",
212        HostFunction::new("concat", 1, string_concat),
213    );
214    ctx.register_builtin(
215        "string_indexOf",
216        HostFunction::new("indexOf", 1, string_index_of),
217    );
218    ctx.register_builtin(
219        "string_lastIndexOf",
220        HostFunction::new("lastIndexOf", 1, string_last_index_of),
221    );
222    ctx.register_builtin("string_slice", HostFunction::new("slice", 2, string_slice));
223    ctx.register_builtin(
224        "string_substring",
225        HostFunction::new("substring", 2, string_substring),
226    );
227    ctx.register_builtin(
228        "string_toString",
229        HostFunction::new("toString", 0, string_to_string),
230    );
231    ctx.register_builtin(
232        "string_toLowerCase",
233        HostFunction::new("toLowerCase", 0, string_to_lower_case),
234    );
235    ctx.register_builtin(
236        "string_toUpperCase",
237        HostFunction::new("toUpperCase", 0, string_to_upper_case),
238    );
239    ctx.register_builtin("string_split", HostFunction::new("split", 1, string_split));
240    ctx.register_builtin(
241        "string_length",
242        HostFunction::new("length", 0, string_length),
243    );
244    ctx.register_builtin(
245        "string_fromCharCode",
246        HostFunction::new("fromCharCode", 1, string_fromcharcode),
247    );
248    ctx.register_builtin(
249        "string_fromCodePoint",
250        HostFunction::new("fromCodePoint", 1, string_fromcodepoint),
251    );
252    ctx.register_builtin("string_trim", HostFunction::new("trim", 0, string_trim));
253    ctx.register_builtin(
254        "string_trimStart",
255        HostFunction::new("trimStart", 0, string_trim_start),
256    );
257    ctx.register_builtin(
258        "string_trimEnd",
259        HostFunction::new("trimEnd", 0, string_trim_end),
260    );
261    ctx.register_builtin(
262        "string_startsWith",
263        HostFunction::new("startsWith", 1, string_starts_with),
264    );
265    ctx.register_builtin(
266        "string_endsWith",
267        HostFunction::new("endsWith", 1, string_ends_with),
268    );
269    ctx.register_builtin(
270        "string_repeat",
271        HostFunction::new("repeat", 1, string_repeat),
272    );
273    ctx.register_builtin(
274        "string_includes",
275        HostFunction::new("includes", 1, string_includes),
276    );
277    ctx.register_builtin(
278        "string_replace",
279        HostFunction::new("replace", 2, string_replace),
280    );
281    ctx.register_builtin(
282        "string_padStart",
283        HostFunction::new("padStart", 1, string_pad_start),
284    );
285    ctx.register_builtin(
286        "string_padEnd",
287        HostFunction::new("padEnd", 1, string_pad_end),
288    );
289    ctx.register_builtin(
290        "string_replaceAll",
291        HostFunction::new("replaceAll", 2, string_replace_all),
292    );
293    ctx.register_builtin(
294        "string_search",
295        HostFunction::new("search", 1, string_search),
296    );
297    ctx.register_builtin("string_match", HostFunction::new("match", 1, string_match));
298    ctx.register_builtin("string_at", HostFunction::new("at", 1, string_at));
299    ctx.register_builtin(
300        "string_isWellFormed",
301        HostFunction::new("isWellFormed", 0, string_is_well_formed),
302    );
303    ctx.register_builtin(
304        "string_toWellFormed",
305        HostFunction::new("toWellFormed", 0, string_to_well_formed),
306    );
307    ctx.register_builtin(
308        "string_codePointAt",
309        HostFunction::new("codePointAt", 1, string_code_point_at),
310    );
311    ctx.register_builtin(
312        "string_matchAll",
313        HostFunction::new("matchAll", 1, string_match_all),
314    );
315    ctx.register_builtin(
316        "string_substr",
317        HostFunction::new("substr", 2, string_substr),
318    );
319}
320
321fn string_constructor(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
322    let s = if args.is_empty() {
323        JSValue::new_string(ctx.intern(""))
324    } else {
325        let val = &args[0];
326        if val.is_string() {
327            val.clone()
328        } else if val.is_int() {
329            JSValue::new_string(ctx.intern(&val.get_int().to_string()))
330        } else if val.is_float() {
331            JSValue::new_string(ctx.intern(&val.get_float().to_string()))
332        } else if val.is_bigint() {
333            let obj = unsafe { crate::value::JSValue::object_from_ptr(val.get_ptr()) };
334            let n = obj.get_bigint_value();
335            JSValue::new_string(ctx.intern(&n.to_string()))
336        } else if val.is_bool() {
337            JSValue::new_string(ctx.intern(&val.get_bool().to_string()))
338        } else if val.is_null() {
339            JSValue::new_string(ctx.common_atoms.null)
340        } else if val.is_undefined() {
341            JSValue::new_string(ctx.common_atoms.undefined)
342        } else if val.is_object() {
343            JSValue::new_string(ctx.intern("[object Object]"))
344        } else {
345            JSValue::new_string(ctx.intern(""))
346        }
347    };
348    s
349}
350
351fn char_count(s: &str) -> usize {
352    s.chars().count()
353}
354
355fn byte_index_for_char_pos(s: &str, pos: usize) -> usize {
356    if pos == 0 {
357        return 0;
358    }
359    match s.char_indices().nth(pos) {
360        Some((idx, _)) => idx,
361        None => s.len(),
362    }
363}
364
365fn string_char_at(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
366    if args.is_empty() {
367        return JSValue::new_string(ctx.intern(""));
368    }
369
370    let atom = if args[0].is_string() {
371        args[0].get_atom()
372    } else {
373        return JSValue::new_string(ctx.intern(""));
374    };
375
376    let index = if args.len() > 1 {
377        args[1].get_int() as usize
378    } else {
379        0
380    };
381
382    if index >= ctx.string_char_count(atom) {
383        return JSValue::new_string(ctx.intern(""));
384    }
385
386    match ctx.string_char_code_at(atom, index) {
387        Some(code) => {
388            let c = char::from_u32(code).unwrap_or('\0');
389            let mut buf = [0u8; 4];
390            let s = c.encode_utf8(&mut buf);
391            JSValue::new_string(ctx.intern(s))
392        }
393        None => JSValue::new_string(ctx.intern("")),
394    }
395}
396
397fn string_char_code_at(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
398    if args.is_empty() {
399        return JSValue::new_float(f64::NAN);
400    }
401
402    let atom = if args[0].is_string() {
403        args[0].get_atom()
404    } else {
405        return JSValue::new_float(f64::NAN);
406    };
407
408    let index = if args.len() > 1 {
409        let v = args[1];
410        if v.is_int() {
411            let i = v.get_int();
412            if i < 0 {
413                return JSValue::new_float(f64::NAN);
414            }
415            i as usize
416        } else if v.is_float() {
417            let f = v.get_float();
418            if f < 0.0 || f.is_nan() || f.is_infinite() {
419                return JSValue::new_float(f64::NAN);
420            }
421            f as usize
422        } else {
423            0
424        }
425    } else {
426        0
427    };
428
429    match ctx.string_char_code_at(atom, index) {
430        Some(code) => JSValue::new_int(code as i64),
431        None => JSValue::new_float(f64::NAN),
432    }
433}
434
435fn string_concat(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
436    if args.is_empty() {
437        return JSValue::new_string(ctx.intern(""));
438    }
439
440    let mut result = if args[0].is_string() {
441        ctx.get_atom_str(args[0].get_atom()).to_string()
442    } else {
443        String::new()
444    };
445
446    for arg in args.iter().skip(1) {
447        if arg.is_string() {
448            result.push_str(ctx.get_atom_str(arg.get_atom()));
449        } else if arg.is_int() {
450            result.push_str(&arg.get_int().to_string());
451        } else if arg.is_float() {
452            result.push_str(&arg.get_float().to_string());
453        } else if arg.is_bool() {
454            result.push_str(&arg.get_bool().to_string());
455        }
456    }
457
458    JSValue::new_string(ctx.intern(&result))
459}
460
461fn string_index_of(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
462    if args.len() < 2 {
463        return JSValue::new_int(-1);
464    }
465
466    let s = if args[0].is_string() {
467        ctx.get_atom_str(args[0].get_atom()).to_string()
468    } else {
469        return JSValue::new_int(-1);
470    };
471
472    let search = if args[1].is_string() {
473        ctx.get_atom_str(args[1].get_atom()).to_string()
474    } else {
475        return JSValue::new_int(-1);
476    };
477
478    let from_index = if args.len() > 2 {
479        args[2].get_int().max(0) as usize
480    } else {
481        0
482    };
483
484    let s_char_len = char_count(&s);
485    if from_index > s_char_len {
486        return JSValue::new_int(-1);
487    }
488
489    let from_byte = byte_index_for_char_pos(&s, from_index);
490
491    match s[from_byte..].find(&search) {
492        Some(byte_pos) => {
493            let byte_idx = from_byte + byte_pos;
494            let char_idx = s[..byte_idx].chars().count();
495            JSValue::new_int(char_idx as i64)
496        }
497        None => JSValue::new_int(-1),
498    }
499}
500
501fn string_last_index_of(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
502    if args.len() < 2 {
503        return JSValue::new_int(-1);
504    }
505
506    let s = if args[0].is_string() {
507        ctx.get_atom_str(args[0].get_atom()).to_string()
508    } else {
509        return JSValue::new_int(-1);
510    };
511
512    let search = if args[1].is_string() {
513        ctx.get_atom_str(args[1].get_atom()).to_string()
514    } else {
515        return JSValue::new_int(-1);
516    };
517
518    match s.rfind(&search) {
519        Some(byte_pos) => {
520            let char_idx = s[..byte_pos].chars().count();
521            JSValue::new_int(char_idx as i64)
522        }
523        None => JSValue::new_int(-1),
524    }
525}
526
527fn string_substring(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
528    if args.is_empty() {
529        return JSValue::new_string(ctx.intern(""));
530    }
531
532    let s = if args[0].is_string() {
533        ctx.get_atom_str(args[0].get_atom()).to_string()
534    } else {
535        return JSValue::new_string(ctx.intern(""));
536    };
537
538    let len = char_count(&s);
539    let start = if args.len() > 1 {
540        let v = args[1].get_int().max(0) as usize;
541        v.min(len)
542    } else {
543        0
544    };
545
546    let end = if args.len() > 2 {
547        let v = args[2].get_int().max(0) as usize;
548        v.min(len)
549    } else {
550        len
551    };
552
553    let (start, end) = if start > end {
554        (end, start)
555    } else {
556        (start, end)
557    };
558
559    let start_b = byte_index_for_char_pos(&s, start);
560    let end_b = byte_index_for_char_pos(&s, end);
561    JSValue::new_string(ctx.intern(&s[start_b..end_b]))
562}
563
564fn string_slice(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
565    if args.is_empty() {
566        return JSValue::new_string(ctx.intern(""));
567    }
568
569    let s = if args[0].is_string() {
570        ctx.get_atom_str(args[0].get_atom()).to_string()
571    } else {
572        return JSValue::new_string(ctx.intern(""));
573    };
574
575    let len = char_count(&s);
576    let start = if args.len() > 1 {
577        let v = args[1].get_int();
578        if v < 0 {
579            (len as i64 + v).max(0) as usize
580        } else {
581            (v as usize).min(len)
582        }
583    } else {
584        0
585    };
586
587    let end = if args.len() > 2 {
588        let v = args[2].get_int();
589        if v < 0 {
590            (len as i64 + v).max(0) as usize
591        } else {
592            (v as usize).min(len)
593        }
594    } else {
595        len
596    };
597
598    if start >= end {
599        return JSValue::new_string(ctx.intern(""));
600    }
601
602    let start_b = byte_index_for_char_pos(&s, start);
603    let end_b = byte_index_for_char_pos(&s, end);
604    JSValue::new_string(ctx.intern(&s[start_b..end_b]))
605}
606
607fn string_to_string(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
608    if args.is_empty() {
609        return JSValue::new_string(ctx.intern(""));
610    }
611
612    if args[0].is_string() {
613        return args[0].clone();
614    }
615
616    if args[0].is_object() {
617        let obj = args[0].as_object();
618        let prim = obj.get(ctx.common_atoms.__value__);
619        if let Some(v) = prim {
620            if v.is_string() {
621                return v;
622            }
623        }
624    }
625    args[0].clone()
626}
627
628fn string_to_lower_case(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
629    if args.is_empty() {
630        return JSValue::new_string(ctx.intern(""));
631    }
632
633    let s = if args[0].is_string() {
634        ctx.get_atom_str(args[0].get_atom()).to_lowercase()
635    } else {
636        return JSValue::new_string(ctx.intern(""));
637    };
638
639    JSValue::new_string(ctx.intern(&s))
640}
641
642fn string_to_upper_case(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
643    if args.is_empty() {
644        return JSValue::new_string(ctx.intern(""));
645    }
646
647    let s = if args[0].is_string() {
648        ctx.get_atom_str(args[0].get_atom()).to_uppercase()
649    } else {
650        return JSValue::new_string(ctx.intern(""));
651    };
652
653    JSValue::new_string(ctx.intern(&s))
654}
655
656fn string_split(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
657    if args.is_empty() {
658        let mut result = JSObject::new_array();
659        result.set(ctx.common_atoms.length, JSValue::new_int(0));
660        let ptr = Box::into_raw(Box::new(result)) as usize;
661        return JSValue::new_object(ptr);
662    }
663
664    let s_atom = if args[0].is_string() {
665        args[0].get_atom()
666    } else {
667        let mut result = JSObject::new_array();
668        result.set(ctx.common_atoms.length, JSValue::new_int(0));
669        let ptr = Box::into_raw(Box::new(result)) as usize;
670        return JSValue::new_object(ptr);
671    };
672
673    let length_atom = ctx.common_atoms.length;
674
675    if args.len() > 1 && args[1].is_object() {
676        let sep_obj = args[1].as_object();
677        let pattern_atom = ctx.common_atoms.__pattern__;
678        if sep_obj.get(pattern_atom).is_some() {
679            let s = ctx.get_atom_str(s_atom).to_string();
680            let limit = if args.len() > 2 && args[2].is_int() {
681                args[2].get_int() as usize
682            } else {
683                usize::MAX
684            };
685
686            let mut parts: Vec<String> = Vec::new();
687            let compiled_re = args[1]
688                .as_object()
689                .get_compiled_regex()
690                .map(|re| re as *const crate::regexp::Regex);
691            if let Some(re_ptr) = compiled_re {
692                let re = unsafe { &*re_ptr };
693                let mut last = 0usize;
694                let mut search_start = 0usize;
695                loop {
696                    if parts.len() >= limit {
697                        break;
698                    }
699                    if search_start > s.len() {
700                        break;
701                    }
702                    let sub = &s[search_start..];
703                    if let Some(m) = re.find(sub) {
704                        let match_start = search_start + m.start();
705                        let match_end = search_start + m.end();
706                        if match_end == search_start && match_start == last {
707                            search_start += s[search_start..]
708                                .chars()
709                                .next()
710                                .map(|c| c.len_utf8())
711                                .unwrap_or(1);
712                            continue;
713                        }
714                        parts.push(s[last..match_start].to_string());
715
716                        for cap in m.iter().skip(1) {
717                            parts.push(cap.unwrap_or("").to_string());
718                        }
719                        last = match_end;
720                        search_start = match_end;
721                        if match_end == match_start {
722                            search_start += s[search_start..]
723                                .chars()
724                                .next()
725                                .map(|c| c.len_utf8())
726                                .unwrap_or(1);
727                        }
728                    } else {
729                        break;
730                    }
731                }
732                if parts.len() < limit {
733                    parts.push(s[last..].to_string());
734                }
735            } else {
736                parts.push(s);
737            }
738
739            let mut result = JSObject::new_array();
740            for (i, part) in parts.iter().enumerate() {
741                let key = ctx.int_atom_mut(i);
742                let part_atom = ctx.intern(part);
743                result.set(key, JSValue::new_string(part_atom));
744            }
745            result.set(length_atom, JSValue::new_int(parts.len() as i64));
746            let ptr = Box::into_raw(Box::new(result)) as usize;
747            return JSValue::new_object(ptr);
748        }
749    }
750
751    let sep_atom = if args.len() > 1 && args[1].is_string() {
752        Some(args[1].get_atom())
753    } else {
754        None
755    };
756
757    let s = ctx.get_atom_str(s_atom);
758    let separator = sep_atom.map(|a| ctx.get_atom_str(a)).unwrap_or("");
759
760    let mut result = JSObject::new_array();
761
762    if separator.is_empty() {
763        let chars: Vec<char> = s.chars().collect();
764        for (i, c) in chars.iter().enumerate() {
765            let key = ctx.int_atom_mut(i);
766            let c_str = c.to_string();
767            let c_atom = ctx.intern(&c_str);
768            result.set(key, JSValue::new_string(c_atom));
769        }
770        result.set(length_atom, JSValue::new_int(chars.len() as i64));
771    } else {
772        let parts: Vec<String> = s.split(separator).map(|s| s.to_string()).collect();
773        for (i, part) in parts.iter().enumerate() {
774            let key = ctx.int_atom_mut(i);
775            let part_atom = ctx.intern(part);
776            result.set(key, JSValue::new_string(part_atom));
777        }
778        result.set(length_atom, JSValue::new_int(parts.len() as i64));
779    }
780
781    let ptr = Box::into_raw(Box::new(result)) as usize;
782    JSValue::new_object(ptr)
783}
784
785fn string_length(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
786    if args.is_empty() {
787        return JSValue::new_int(0);
788    }
789
790    if args[0].is_string() {
791        JSValue::new_int(ctx.string_char_count(args[0].get_atom()) as i64)
792    } else {
793        JSValue::new_int(0)
794    }
795}
796
797fn from_str_or_hex(s: &str) -> u32 {
798    let s = s.trim();
799    if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
800        u32::from_str_radix(hex, 16).unwrap_or(0)
801    } else {
802        s.parse::<f64>().unwrap_or(0.0) as u32
803    }
804}
805
806fn string_fromcharcode(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
807    let mut result = String::new();
808    for arg in args {
809        let code = if arg.is_string() {
810            from_str_or_hex(ctx.get_atom_str(arg.get_atom()))
811        } else if arg.is_int() {
812            arg.get_int() as u32
813        } else {
814            0
815        };
816        if let Some(c) = char::from_u32(code & 0xFFFF) {
817            result.push(c);
818        }
819    }
820    JSValue::new_string(ctx.intern(&result))
821}
822
823fn string_fromcodepoint(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
824    let mut result = String::new();
825    for arg in args {
826        let code = arg.get_int() as u32;
827        if let Some(c) = char::from_u32(code) {
828            result.push(c);
829        }
830    }
831    JSValue::new_string(ctx.intern(&result))
832}
833
834fn string_trim(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
835    if args.is_empty() {
836        return JSValue::new_string(ctx.intern(""));
837    }
838    let s = ctx.get_atom_str(args[0].get_atom()).to_string();
839    let trimmed = s.trim();
840    JSValue::new_string(ctx.intern(trimmed))
841}
842
843fn string_trim_start(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
844    if args.is_empty() {
845        return JSValue::new_string(ctx.intern(""));
846    }
847    let s = ctx.get_atom_str(args[0].get_atom()).to_string();
848    let trimmed = s.trim_start();
849    JSValue::new_string(ctx.intern(trimmed))
850}
851
852fn string_trim_end(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
853    if args.is_empty() {
854        return JSValue::new_string(ctx.intern(""));
855    }
856    let s = ctx.get_atom_str(args[0].get_atom()).to_string();
857    let trimmed = s.trim_end();
858    JSValue::new_string(ctx.intern(trimmed))
859}
860
861fn string_starts_with(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
862    if args.len() < 2 {
863        return JSValue::bool(false);
864    }
865    let s = ctx.get_atom_str(args[0].get_atom()).to_string();
866    let prefix = ctx.get_atom_str(args[1].get_atom()).to_string();
867    JSValue::bool(s.starts_with(&prefix))
868}
869
870fn string_ends_with(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
871    if args.len() < 2 {
872        return JSValue::bool(false);
873    }
874    let s = ctx.get_atom_str(args[0].get_atom()).to_string();
875    let suffix = ctx.get_atom_str(args[1].get_atom()).to_string();
876    JSValue::bool(s.ends_with(&suffix))
877}
878
879fn string_repeat(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
880    if args.len() < 2 {
881        return JSValue::new_string(ctx.intern(""));
882    }
883    let s = ctx.get_atom_str(args[0].get_atom()).to_string();
884    let count = args[1].get_int();
885    if count < 0 {
886        return JSValue::new_string(ctx.intern(""));
887    }
888    let count = count as usize;
889    let repeated = s.repeat(count);
890    JSValue::new_string(ctx.intern(&repeated))
891}
892
893fn string_includes(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
894    if args.len() < 2 {
895        return JSValue::bool(false);
896    }
897    let s = ctx.get_atom_str(args[0].get_atom()).to_string();
898    let search = ctx.get_atom_str(args[1].get_atom()).to_string();
899    JSValue::bool(s.contains(&search))
900}
901
902fn string_replace(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
903    if args.len() < 3 {
904        if args.is_empty() {
905            return JSValue::new_string(ctx.intern(""));
906        }
907        return args[0].clone();
908    }
909    let s = if args[0].is_string() {
910        ctx.get_atom_str(args[0].get_atom()).to_string()
911    } else {
912        return args[0].clone();
913    };
914
915    if args[1].is_object() {
916        let regexp_obj = args[1].as_object();
917
918        let pattern_atom = ctx.common_atoms.__pattern__;
919        let flags_atom = ctx.common_atoms.__flags__;
920
921        let pattern = if let Some(p) = regexp_obj.get(pattern_atom) {
922            if p.is_string() {
923                ctx.get_atom_str(p.get_atom()).to_string()
924            } else {
925                return JSValue::new_string(ctx.intern(&s));
926            }
927        } else {
928            return JSValue::new_string(ctx.intern(&s));
929        };
930
931        let flags = if let Some(f) = regexp_obj.get(flags_atom) {
932            if f.is_string() {
933                ctx.get_atom_str(f.get_atom()).to_string()
934            } else {
935                String::new()
936            }
937        } else {
938            String::new()
939        };
940
941        let replacement = if args[2].is_string() {
942            ctx.get_atom_str(args[2].get_atom()).to_string()
943        } else {
944            String::new()
945        };
946
947        let ignore_case = flags.contains('i');
948        let is_global = flags.contains('g');
949
950        if is_global {
951            let mut result = String::new();
952            let mut last_end = 0;
953            let mut search_from = 0;
954            let pat_lower = if ignore_case {
955                pattern.to_lowercase()
956            } else {
957                String::new()
958            };
959            let s_lower = if ignore_case {
960                s.to_lowercase()
961            } else {
962                String::new()
963            };
964
965            loop {
966                let found = if ignore_case {
967                    s_lower[search_from..]
968                        .find(&pat_lower)
969                        .map(|i| i + search_from)
970                } else {
971                    s[search_from..].find(&pattern).map(|i| i + search_from)
972                };
973
974                if let Some(pos) = found {
975                    result.push_str(&s[last_end..pos]);
976                    result.push_str(&replacement);
977                    last_end = pos + pattern.len();
978                    search_from = last_end;
979                    if pattern.is_empty() {
980                        if search_from < s.len() {
981                            result.push_str(&s[search_from..search_from + 1]);
982                            search_from += 1;
983                            last_end = search_from;
984                        } else {
985                            break;
986                        }
987                    }
988                } else {
989                    break;
990                }
991            }
992            result.push_str(&s[last_end..]);
993            JSValue::new_string(ctx.intern(&result))
994        } else {
995            let found = if ignore_case {
996                s.to_lowercase().find(&pattern.to_lowercase())
997            } else {
998                s.find(&pattern)
999            };
1000
1001            if let Some(pos) = found {
1002                let mut result = String::new();
1003                result.push_str(&s[..pos]);
1004                result.push_str(&replacement);
1005                result.push_str(&s[pos + pattern.len()..]);
1006                JSValue::new_string(ctx.intern(&result))
1007            } else {
1008                JSValue::new_string(ctx.intern(&s))
1009            }
1010        }
1011    } else {
1012        let search = if args[1].is_string() {
1013            ctx.get_atom_str(args[1].get_atom()).to_string()
1014        } else {
1015            return JSValue::new_string(ctx.intern(&s));
1016        };
1017        let replacement = if args[2].is_string() {
1018            ctx.get_atom_str(args[2].get_atom()).to_string()
1019        } else if args[2].is_function() {
1020            if let Some(vm_ptr) = ctx.get_register_vm_ptr() {
1021                let vm = unsafe { &mut *(vm_ptr as *mut crate::runtime::vm::VM) };
1022                let result = vm.call_function_with_this(
1023                    ctx,
1024                    args[2],
1025                    JSValue::undefined(),
1026                    &[args[1].clone(), JSValue::new_int(0), args[0].clone()],
1027                );
1028                match result {
1029                    Ok(v) => {
1030                        if v.is_string() {
1031                            ctx.get_atom_str(v.get_atom()).to_string()
1032                        } else if v.is_undefined() {
1033                            "undefined".to_string()
1034                        } else if v.is_null() {
1035                            "null".to_string()
1036                        } else {
1037                            format!("{}", v.get_int())
1038                        }
1039                    }
1040                    Err(_) => String::new(),
1041                }
1042            } else {
1043                String::new()
1044            }
1045        } else {
1046            String::new()
1047        };
1048        let result = s.replace(&search, &replacement);
1049        JSValue::new_string(ctx.intern(&result))
1050    }
1051}
1052
1053fn string_pad_start(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
1054    if args.is_empty() {
1055        return JSValue::new_string(ctx.intern(""));
1056    }
1057    let s = ctx.get_atom_str(args[0].get_atom()).to_string();
1058    let target_len = if args.len() > 1 {
1059        args[1].get_int() as usize
1060    } else {
1061        0
1062    };
1063    let pad_str = if args.len() > 2 && args[2].is_string() {
1064        ctx.get_atom_str(args[2].get_atom()).to_string()
1065    } else {
1066        " ".to_string()
1067    };
1068
1069    if target_len <= s.len() {
1070        return args[0];
1071    }
1072
1073    let mut pad_count = target_len - s.len();
1074    let mut prepend = String::new();
1075    while pad_count > 0 {
1076        if pad_count >= pad_str.len() {
1077            prepend.push_str(&pad_str);
1078            pad_count -= pad_str.len();
1079        } else {
1080            prepend.push_str(&pad_str[..pad_count]);
1081            pad_count = 0;
1082        }
1083    }
1084
1085    JSValue::new_string(ctx.intern(&(prepend + &s)))
1086}
1087
1088fn string_pad_end(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
1089    if args.is_empty() {
1090        return JSValue::new_string(ctx.intern(""));
1091    }
1092    let s = ctx.get_atom_str(args[0].get_atom()).to_string();
1093    let target_len = if args.len() > 1 {
1094        args[1].get_int() as usize
1095    } else {
1096        0
1097    };
1098    let pad_str = if args.len() > 2 && args[2].is_string() {
1099        ctx.get_atom_str(args[2].get_atom()).to_string()
1100    } else {
1101        " ".to_string()
1102    };
1103
1104    if target_len <= s.len() {
1105        return args[0];
1106    }
1107
1108    let mut pad_count = target_len - s.len();
1109    let mut append = String::new();
1110    while pad_count > 0 {
1111        if pad_count >= pad_str.len() {
1112            append.push_str(&pad_str);
1113            pad_count -= pad_str.len();
1114        } else {
1115            append.push_str(&pad_str[..pad_count]);
1116            pad_count = 0;
1117        }
1118    }
1119
1120    JSValue::new_string(ctx.intern(&(s + &append)))
1121}
1122
1123fn string_replace_all(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
1124    if args.len() < 3 {
1125        if args.is_empty() {
1126            return JSValue::new_string(ctx.intern(""));
1127        }
1128        return args[0];
1129    }
1130    let s = if args[0].is_string() {
1131        ctx.get_atom_str(args[0].get_atom()).to_string()
1132    } else {
1133        return args[0];
1134    };
1135    let search = if args[1].is_string() {
1136        ctx.get_atom_str(args[1].get_atom()).to_string()
1137    } else {
1138        return args[0];
1139    };
1140    let replacement = if args[2].is_string() {
1141        ctx.get_atom_str(args[2].get_atom()).to_string()
1142    } else {
1143        String::new()
1144    };
1145
1146    let result = s.replace(&search as &str, &replacement as &str);
1147    JSValue::new_string(ctx.intern(&result))
1148}
1149
1150fn string_at(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
1151    if args.is_empty() {
1152        return JSValue::undefined();
1153    }
1154    let s = if args[0].is_string() {
1155        ctx.get_atom_str(args[0].get_atom()).to_string()
1156    } else {
1157        return JSValue::undefined();
1158    };
1159    let chars: Vec<char> = s.chars().collect();
1160    let len = chars.len();
1161    let idx = if args.len() > 1 { args[1].get_int() } else { 0 };
1162    let actual_idx = if idx < 0 { len as i64 + idx } else { idx };
1163    if actual_idx < 0 || actual_idx as usize >= len {
1164        return JSValue::undefined();
1165    }
1166    let c = chars[actual_idx as usize];
1167    JSValue::new_string(ctx.intern(&c.to_string()))
1168}
1169
1170fn string_is_well_formed(_ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
1171    if args.is_empty() {
1172        return JSValue::bool(false);
1173    }
1174    let this = args[0];
1175    if !this.is_string() {
1176        return JSValue::bool(false);
1177    }
1178
1179    JSValue::bool(true)
1180}
1181
1182fn string_to_well_formed(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
1183    if args.is_empty() {
1184        return JSValue::undefined();
1185    }
1186    let this = args[0];
1187    if !this.is_string() {
1188        return this;
1189    }
1190    let s = ctx.get_atom_str(this.get_atom()).to_string();
1191
1192    JSValue::new_string(ctx.intern(&s))
1193}
1194
1195fn string_code_point_at(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
1196    if args.is_empty() {
1197        return JSValue::undefined();
1198    }
1199    let this = args[0];
1200    if !this.is_string() {
1201        return JSValue::undefined();
1202    }
1203    let s = ctx.get_atom_str(this.get_atom()).to_string();
1204    let chars: Vec<char> = s.chars().collect();
1205    let len = chars.len();
1206    let index = if args.len() > 1 {
1207        args[1].get_int() as i64
1208    } else {
1209        0
1210    };
1211    let actual_index = if index < 0 { len as i64 + index } else { index };
1212    if actual_index < 0 || actual_index as usize >= len {
1213        return JSValue::undefined();
1214    }
1215    let c = chars[actual_index as usize];
1216    JSValue::new_int(c as u32 as i64)
1217}
1218
1219fn string_match_all(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
1220    if args.is_empty() {
1221        let mut result = JSObject::new_array();
1222        result.set(ctx.common_atoms.length, JSValue::new_int(0));
1223        let ptr = Box::into_raw(Box::new(result)) as usize;
1224        return JSValue::new_object(ptr);
1225    }
1226
1227    let this = args[0];
1228    let s = if this.is_string() {
1229        ctx.get_atom_str(this.get_atom()).to_string()
1230    } else {
1231        let mut result = JSObject::new_array();
1232        result.set(ctx.common_atoms.length, JSValue::new_int(0));
1233        let ptr = Box::into_raw(Box::new(result)) as usize;
1234        return JSValue::new_object(ptr);
1235    };
1236
1237    let pattern = if args.len() > 1 {
1238        if args[1].is_object() {
1239            let regexp_obj = args[1].as_object();
1240
1241            let flags_atom = ctx.common_atoms.__flags__;
1242            let flags = if let Some(f) = regexp_obj.get(flags_atom) {
1243                if f.is_string() {
1244                    ctx.get_atom_str(f.get_atom()).to_string()
1245                } else {
1246                    String::new()
1247                }
1248            } else {
1249                String::new()
1250            };
1251
1252            if !flags.contains('g') {
1253                let mut err_obj = JSObject::new();
1254                err_obj.set(
1255                    ctx.common_atoms.name,
1256                    JSValue::new_string(ctx.intern("TypeError")),
1257                );
1258                err_obj.set(
1259                    ctx.common_atoms.message,
1260                    JSValue::new_string(ctx.intern("matchAll requires a global RegExp")),
1261                );
1262                let ptr = Box::into_raw(Box::new(err_obj)) as usize;
1263                return JSValue::new_object(ptr);
1264            }
1265
1266            let pattern_atom = ctx.common_atoms.__pattern__;
1267            if let Some(p) = regexp_obj.get(pattern_atom) {
1268                if p.is_string() {
1269                    ctx.get_atom_str(p.get_atom()).to_string()
1270                } else {
1271                    String::new()
1272                }
1273            } else {
1274                String::new()
1275            }
1276        } else if args[1].is_string() {
1277            ctx.get_atom_str(args[1].get_atom()).to_string()
1278        } else {
1279            String::new()
1280        }
1281    } else {
1282        String::new()
1283    };
1284
1285    let mut result_array = JSObject::new_array();
1286    let length_atom = ctx.common_atoms.length;
1287    let mut match_count = 0;
1288
1289    if pattern.is_empty() {
1290        result_array.set(length_atom, JSValue::new_int(0));
1291        let ptr = Box::into_raw(Box::new(result_array)) as usize;
1292        return JSValue::new_object(ptr);
1293    }
1294
1295    let mut search_pos = 0;
1296    while search_pos <= s.len() {
1297        let max_start = if pattern.len() <= s.len() {
1298            s.len() - pattern.len()
1299        } else {
1300            break;
1301        };
1302
1303        let mut found = false;
1304        for i in search_pos..=max_start {
1305            if i + pattern.len() <= s.len() {
1306                let slice = &s[i..i + pattern.len()];
1307                if slice == pattern {
1308                    found = true;
1309                    let mut match_obj = JSObject::new();
1310                    match_obj.set(ctx.intern("0"), JSValue::new_string(ctx.intern(slice)));
1311                    match_obj.set(ctx.common_atoms.index, JSValue::new_int(i as i64));
1312                    match_obj.set(ctx.common_atoms.input, JSValue::new_string(ctx.intern(&s)));
1313                    match_obj.set(ctx.common_atoms.length, JSValue::new_int(1));
1314
1315                    let key = ctx.int_atom_mut(match_count);
1316                    result_array.set(
1317                        key,
1318                        JSValue::new_object(Box::into_raw(Box::new(match_obj)) as usize),
1319                    );
1320
1321                    match_count += 1;
1322                    search_pos = i + pattern.len();
1323                    if pattern.len() == 0 {
1324                        search_pos += 1;
1325                    }
1326                    break;
1327                }
1328            }
1329        }
1330
1331        if !found {
1332            break;
1333        }
1334    }
1335
1336    result_array.set(length_atom, JSValue::new_int(match_count as i64));
1337    let ptr = Box::into_raw(Box::new(result_array)) as usize;
1338    JSValue::new_object(ptr)
1339}
1340
1341fn string_search(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
1342    if args.len() < 2 {
1343        return JSValue::new_int(-1);
1344    }
1345    let s = if args[0].is_string() {
1346        ctx.get_atom_str(args[0].get_atom()).to_string()
1347    } else {
1348        return JSValue::new_int(-1);
1349    };
1350
1351    if args[1].is_object() {
1352        let regexp_obj = args[1].as_object();
1353        let pattern_atom = ctx.common_atoms.__pattern__;
1354        let flags_atom = ctx.common_atoms.__flags__;
1355
1356        let pattern = if let Some(p) = regexp_obj.get(pattern_atom) {
1357            if p.is_string() {
1358                ctx.get_atom_str(p.get_atom()).to_string()
1359            } else {
1360                return JSValue::new_int(-1);
1361            }
1362        } else {
1363            return JSValue::new_int(-1);
1364        };
1365
1366        let flags = if let Some(f) = regexp_obj.get(flags_atom) {
1367            if f.is_string() {
1368                ctx.get_atom_str(f.get_atom()).to_string()
1369            } else {
1370                String::new()
1371            }
1372        } else {
1373            String::new()
1374        };
1375
1376        let ignore_case = flags.contains('i');
1377        let s_lower = if ignore_case {
1378            s.to_lowercase()
1379        } else {
1380            String::new()
1381        };
1382        let p_lower = if ignore_case {
1383            pattern.to_lowercase()
1384        } else {
1385            String::new()
1386        };
1387
1388        let found = if ignore_case {
1389            s_lower.find(&p_lower)
1390        } else {
1391            s.find(&pattern)
1392        };
1393        JSValue::new_int(found.map(|i| i as i64).unwrap_or(-1))
1394    } else if args[1].is_string() {
1395        let search = ctx.get_atom_str(args[1].get_atom()).to_string();
1396        JSValue::new_int(s.find(&search).map(|i| i as i64).unwrap_or(-1))
1397    } else {
1398        JSValue::new_int(-1)
1399    }
1400}
1401
1402fn string_match(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
1403    if args.len() < 2 {
1404        return JSValue::null();
1405    }
1406    let s = if args[0].is_string() {
1407        ctx.get_atom_str(args[0].get_atom()).to_string()
1408    } else {
1409        return JSValue::null();
1410    };
1411
1412    if args[1].is_object() {
1413        let regexp_obj = args[1].as_object();
1414        let pattern_atom = ctx.common_atoms.__pattern__;
1415        let flags_atom = ctx.common_atoms.__flags__;
1416
1417        let pattern = if let Some(p) = regexp_obj.get(pattern_atom) {
1418            if p.is_string() {
1419                ctx.get_atom_str(p.get_atom()).to_string()
1420            } else {
1421                return JSValue::null();
1422            }
1423        } else {
1424            return JSValue::null();
1425        };
1426
1427        let flags = if let Some(f) = regexp_obj.get(flags_atom) {
1428            if f.is_string() {
1429                ctx.get_atom_str(f.get_atom()).to_string()
1430            } else {
1431                String::new()
1432            }
1433        } else {
1434            String::new()
1435        };
1436
1437        let is_global = flags.contains('g');
1438        let ignore_case = flags.contains('i');
1439        let s_lower = if ignore_case {
1440            s.to_lowercase()
1441        } else {
1442            String::new()
1443        };
1444        let p_lower = if ignore_case {
1445            pattern.to_lowercase()
1446        } else {
1447            String::new()
1448        };
1449
1450        if is_global {
1451            let mut results: Vec<String> = Vec::new();
1452            let mut search_from = 0;
1453            loop {
1454                let found = if ignore_case {
1455                    s_lower[search_from..]
1456                        .find(&p_lower)
1457                        .map(|i| i + search_from)
1458                } else {
1459                    s[search_from..].find(&pattern).map(|i| i + search_from)
1460                };
1461                if let Some(pos) = found {
1462                    let end = (pos + pattern.len()).min(s.len());
1463                    results.push(s[pos..end].to_string());
1464                    search_from = end;
1465                    if pattern.is_empty() {
1466                        if search_from < s.len() {
1467                            search_from += 1;
1468                        } else {
1469                            break;
1470                        }
1471                    }
1472                } else {
1473                    break;
1474                }
1475            }
1476            if results.is_empty() {
1477                return JSValue::null();
1478            }
1479            let mut arr = JSObject::new_array();
1480            let length_atom = ctx.common_atoms.length;
1481            for (i, m) in results.iter().enumerate() {
1482                let key = ctx.int_atom_mut(i);
1483                arr.set(key, JSValue::new_string(ctx.intern(m)));
1484            }
1485            arr.set(length_atom, JSValue::new_int(results.len() as i64));
1486            let ptr = Box::into_raw(Box::new(arr)) as usize;
1487            JSValue::new_object(ptr)
1488        } else {
1489            let found = if ignore_case {
1490                s_lower.find(&p_lower)
1491            } else {
1492                s.find(&pattern)
1493            };
1494            if let Some(pos) = found {
1495                let end = (pos + pattern.len()).min(s.len());
1496                let match_str = &s[pos..end];
1497                let mut result = JSObject::new_array();
1498                result.set(ctx.intern("0"), JSValue::new_string(ctx.intern(match_str)));
1499                result.set(ctx.common_atoms.index, JSValue::new_int(pos as i64));
1500                result.set(ctx.common_atoms.input, JSValue::new_string(ctx.intern(&s)));
1501                result.set(ctx.common_atoms.length, JSValue::new_int(1));
1502                let ptr = Box::into_raw(Box::new(result)) as usize;
1503                JSValue::new_object(ptr)
1504            } else {
1505                JSValue::null()
1506            }
1507        }
1508    } else {
1509        JSValue::null()
1510    }
1511}
1512
1513fn string_substr(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
1514    if args.is_empty() {
1515        return JSValue::new_string(ctx.intern(""));
1516    }
1517    let s = if args[0].is_string() {
1518        ctx.get_atom_str(args[0].get_atom()).to_string()
1519    } else {
1520        return JSValue::new_string(ctx.intern(""));
1521    };
1522    let len = char_count(&s);
1523
1524    let start = if args.len() > 1 {
1525        let v = args[1].get_int();
1526        if v < 0 {
1527            (len as i64 + v).max(0) as usize
1528        } else {
1529            (v as usize).min(len)
1530        }
1531    } else {
1532        0
1533    };
1534
1535    let end = if args.len() > 2 {
1536        let v = args[2].get_int();
1537        if v <= 0 {
1538            return JSValue::new_string(ctx.intern(""));
1539        }
1540        (start + v as usize).min(len)
1541    } else {
1542        len
1543    };
1544    let start_b = byte_index_for_char_pos(&s, start);
1545    let end_b = byte_index_for_char_pos(&s, end);
1546    JSValue::new_string(ctx.intern(&s[start_b..end_b]))
1547}