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 to_integer_index(val: &JSValue, ctx: &mut JSContext) -> i64 {
7    if val.is_int() {
8        val.get_int()
9    } else if val.is_float() {
10        val.get_float().trunc() as i64
11    } else if val.is_undefined() || val.is_null() {
12        0
13    } else if val.is_bool() {
14        if val.get_bool() { 1 } else { 0 }
15    } else if val.is_string() {
16        let s = ctx.get_atom_str(val.get_atom());
17        match s.trim().parse::<f64>() {
18            Ok(v) if !v.is_nan() => v.trunc() as i64,
19            _ => 0,
20        }
21    } else {
22        0
23    }
24}
25
26fn create_builtin_function(ctx: &mut JSContext, name: &str) -> JSValue {
27    let arity = ctx.get_builtin_arity(name).unwrap_or(1);
28    let mut func = crate::object::function::JSFunction::new_builtin(ctx.intern(name), arity);
29    func.set_builtin_marker(ctx, name);
30    let ptr = Box::into_raw(Box::new(func)) as usize;
31    ctx.runtime_mut().gc_heap_mut().track_function(ptr);
32    JSValue::new_function(ptr)
33}
34
35pub fn js_float_to_string(f: f64) -> String {
36    if f.is_infinite() {
37        return if f.is_sign_positive() {
38            "Infinity".to_string()
39        } else {
40            "-Infinity".to_string()
41        };
42    }
43    if f.is_nan() {
44        return "NaN".to_string();
45    }
46    format!("{}", f)
47}
48
49fn this_to_string(ctx: &mut JSContext, this: &JSValue) -> Option<String> {
50    if this.is_string() {
51        return Some(ctx.get_atom_str(this.get_atom()).to_string());
52    }    if this.is_undefined() || this.is_null() {
53        let mut err = JSObject::new();
54        err.set(
55            ctx.common_atoms.name,
56            JSValue::new_string(ctx.intern("TypeError")),
57        );
58        err.set(
59            ctx.common_atoms.message,
60            JSValue::new_string(ctx.intern("this is not coercible")),
61        );
62        if let Some(proto) = ctx.get_type_error_prototype() {
63            err.prototype = Some(proto);
64        }
65        let ptr = Box::into_raw(Box::new(err)) as usize;
66        ctx.runtime_mut().gc_heap_mut().track(ptr);
67        ctx.pending_exception = Some(JSValue::new_object(ptr));
68        return None;
69    }
70    if this.is_bool() {
71        return Some(if this.get_bool() {
72            "true".to_string()
73        } else {
74            "false".to_string()
75        });
76    }
77    if this.is_int() {
78        return Some(format!("{}", this.get_int()));
79    }
80    if this.is_float() {
81        return Some(js_float_to_string(this.get_float()));
82    }
83    if this.is_object() {
84        let obj = this.as_object();
85        if let Some(v) = obj.get(ctx.common_atoms.__value__) {
86            if v.is_string() {
87                return Some(ctx.get_atom_str(v.get_atom()).to_string());
88            }
89            if v.is_int() {
90                return Some(format!("{}", v.get_int()));
91            }
92            if v.is_float() {
93                return Some(js_float_to_string(v.get_float()));
94            }
95            if v.is_bool() {
96                return Some(if v.get_bool() {
97                    "true".to_string()
98                } else {
99                    "false".to_string()
100                });
101            }
102        }
103        if let Some(vm_ptr) = ctx.get_register_vm_ptr() {
104            let ts_fn = obj.get(ctx.common_atoms.to_string);
105            if let Some(ts) = ts_fn {
106                if ts.is_function() {
107                    let vm = unsafe { &mut *(vm_ptr as *mut crate::runtime::vm::VM) };
108                    if let Ok(r) = vm.call_function_with_this(ctx, ts, *this, &[]) {
109                        if r.is_string() {
110                            return Some(ctx.get_atom_str(r.get_atom()).to_string());
111                        }
112                        if r.is_int() {
113                            return Some(format!("{}", r.get_int()));
114                        }
115                        if r.is_float() {
116                            return Some(js_float_to_string(r.get_float()));
117                        }
118                        if r.is_bool() {
119                            return Some(if r.get_bool() {
120                                "true".to_string()
121                            } else {
122                                "false".to_string()
123                            });
124                        }
125                    }
126                }
127            }
128        }
129        return Some("[object Object]".to_string());
130    }
131    Some(format!("{}", this.to_number()))
132}
133
134fn require_string_coercible(ctx: &mut JSContext, this: &JSValue) -> Option<String> {
135    if this.is_undefined() || this.is_null() {
136        let mut err = JSObject::new();
137        err.set(
138            ctx.common_atoms.name,
139            JSValue::new_string(ctx.intern("TypeError")),
140        );
141        err.set(
142            ctx.common_atoms.message,
143            JSValue::new_string(ctx.intern("this is not coercible")),
144        );
145        if let Some(proto) = ctx.get_type_error_prototype() {
146            err.prototype = Some(proto);
147        }
148        let ptr = Box::into_raw(Box::new(err)) as usize;
149        ctx.runtime_mut().gc_heap_mut().track(ptr);
150        ctx.pending_exception = Some(JSValue::new_object(ptr));
151        return None;
152    }
153    if this.is_string() {
154        return Some(ctx.get_atom_str(this.get_atom()).to_string());
155    }
156    if this.is_bool() {
157        return Some(if this.get_bool() {
158            "true".to_string()
159        } else {
160            "false".to_string()
161        });
162    }
163    if this.is_int() {
164        return Some(format!("{}", this.get_int()));
165    }
166    if this.is_float() {
167        return Some(js_float_to_string(this.get_float()));
168    }
169    if this.is_object() {
170        return this_to_string(ctx, this);
171    }
172    Some(ctx.get_atom_str(this.get_atom()).to_string())
173}
174
175pub fn init_string(ctx: &mut JSContext) {
176    fn set_ne(
177        obj: &mut crate::object::object::JSObject,
178        key: crate::runtime::atom::Atom,
179        val: crate::value::JSValue,
180    ) {
181        obj.define_property(
182            key,
183            crate::object::object::PropertyDescriptor {
184                value: Some(val),
185                writable: true,
186                enumerable: false,
187                configurable: true,
188                get: None,
189                set: None,
190            },
191        );
192    }
193
194    let string_atom = ctx.common_atoms.string;
195
196    let mut string_func = crate::object::function::JSFunction::new_builtin(string_atom, 1);
197    string_func.set_builtin_marker(ctx, "string_constructor");
198
199    string_func.base.define_property(
200        ctx.intern("fromCharCode"),
201        crate::object::object::PropertyDescriptor {
202            value: Some(create_builtin_function(ctx, "string_fromCharCode")),
203            writable: true,
204            enumerable: false,
205            configurable: true,
206            get: None,
207            set: None,
208        },
209    );
210    string_func.base.define_property(
211        ctx.intern("fromCodePoint"),
212        crate::object::object::PropertyDescriptor {
213            value: Some(create_builtin_function(ctx, "string_fromCodePoint")),
214            writable: true,
215            enumerable: false,
216            configurable: true,
217            get: None,
218            set: None,
219        },
220    );
221    string_func.base.define_property(
222        ctx.intern("raw"),
223        crate::object::object::PropertyDescriptor {
224            value: Some(create_builtin_function(ctx, "string_raw")),
225            writable: true,
226            enumerable: false,
227            configurable: true,
228            get: None,
229            set: None,
230        },
231    );
232
233    let string_ptr = Box::into_raw(Box::new(string_func)) as usize;
234    ctx.runtime_mut().gc_heap_mut().track_function(string_ptr);
235    let string_value = JSValue::new_function(string_ptr);
236    let global = ctx.global();
237    if global.is_object() {
238        let global_obj = global.as_object_mut();
239        global_obj.define_property(
240            string_atom,
241            crate::object::object::PropertyDescriptor {
242                value: Some(string_value),
243                writable: true,
244                enumerable: false,
245                configurable: true,
246                get: None,
247                set: None,
248            },
249        );
250    }
251
252    let proto_atom = ctx.intern("StringPrototype");
253    let mut proto_obj = JSObject::new();
254    set_ne(&mut proto_obj, ctx.intern("constructor"), string_value);
255    set_ne(
256        &mut proto_obj,
257        ctx.intern("charAt"),
258        create_builtin_function(ctx, "string_charAt"),
259    );
260    set_ne(
261        &mut proto_obj,
262        ctx.intern("charCodeAt"),
263        create_builtin_function(ctx, "string_charCodeAt"),
264    );
265    set_ne(
266        &mut proto_obj,
267        ctx.intern("concat"),
268        create_builtin_function(ctx, "string_concat"),
269    );
270    set_ne(
271        &mut proto_obj,
272        ctx.intern("indexOf"),
273        create_builtin_function(ctx, "string_indexOf"),
274    );
275    set_ne(
276        &mut proto_obj,
277        ctx.intern("lastIndexOf"),
278        create_builtin_function(ctx, "string_lastIndexOf"),
279    );
280    set_ne(
281        &mut proto_obj,
282        ctx.intern("localeCompare"),
283        create_builtin_function(ctx, "string_localeCompare"),
284    );
285    set_ne(
286        &mut proto_obj,
287        ctx.intern("normalize"),
288        create_builtin_function(ctx, "string_normalize"),
289    );
290    set_ne(
291        &mut proto_obj,
292        ctx.intern("slice"),
293        create_builtin_function(ctx, "string_slice"),
294    );
295    set_ne(
296        &mut proto_obj,
297        ctx.intern("substring"),
298        create_builtin_function(ctx, "string_substring"),
299    );
300    set_ne(
301        &mut proto_obj,
302        ctx.intern("toString"),
303        create_builtin_function(ctx, "string_toString"),
304    );
305    set_ne(
306        &mut proto_obj,
307        ctx.intern("valueOf"),
308        create_builtin_function(ctx, "string_toString"),
309    );
310    set_ne(
311        &mut proto_obj,
312        ctx.intern("toLowerCase"),
313        create_builtin_function(ctx, "string_toLowerCase"),
314    );
315    set_ne(
316        &mut proto_obj,
317        ctx.intern("toUpperCase"),
318        create_builtin_function(ctx, "string_toUpperCase"),
319    );
320    set_ne(
321        &mut proto_obj,
322        ctx.intern("toLocaleLowerCase"),
323        create_builtin_function(ctx, "string_toLocaleLowerCase"),
324    );
325    set_ne(
326        &mut proto_obj,
327        ctx.intern("toLocaleUpperCase"),
328        create_builtin_function(ctx, "string_toLocaleUpperCase"),
329    );
330    set_ne(
331        &mut proto_obj,
332        ctx.intern("split"),
333        create_builtin_function(ctx, "string_split"),
334    );
335    set_ne(
336        &mut proto_obj,
337        ctx.common_atoms.length,
338        create_builtin_function(ctx, "string_length"),
339    );
340    set_ne(
341        &mut proto_obj,
342        ctx.intern("trim"),
343        create_builtin_function(ctx, "string_trim"),
344    );
345    set_ne(
346        &mut proto_obj,
347        ctx.intern("trimStart"),
348        create_builtin_function(ctx, "string_trimStart"),
349    );
350    set_ne(
351        &mut proto_obj,
352        ctx.intern("trimLeft"),
353        create_builtin_function(ctx, "string_trimStart"),
354    );
355    set_ne(
356        &mut proto_obj,
357        ctx.intern("trimEnd"),
358        create_builtin_function(ctx, "string_trimEnd"),
359    );
360    set_ne(
361        &mut proto_obj,
362        ctx.intern("trimRight"),
363        create_builtin_function(ctx, "string_trimEnd"),
364    );
365    set_ne(
366        &mut proto_obj,
367        ctx.intern("startsWith"),
368        create_builtin_function(ctx, "string_startsWith"),
369    );
370    set_ne(
371        &mut proto_obj,
372        ctx.intern("endsWith"),
373        create_builtin_function(ctx, "string_endsWith"),
374    );
375    set_ne(
376        &mut proto_obj,
377        ctx.intern("repeat"),
378        create_builtin_function(ctx, "string_repeat"),
379    );
380    set_ne(
381        &mut proto_obj,
382        ctx.intern("includes"),
383        create_builtin_function(ctx, "string_includes"),
384    );
385    set_ne(
386        &mut proto_obj,
387        ctx.intern("startsWith"),
388        create_builtin_function(ctx, "string_starts_with"),
389    );
390    set_ne(
391        &mut proto_obj,
392        ctx.intern("endsWith"),
393        create_builtin_function(ctx, "string_ends_with"),
394    );
395    set_ne(
396        &mut proto_obj,
397        ctx.intern("replace"),
398        create_builtin_function(ctx, "string_replace"),
399    );
400    set_ne(
401        &mut proto_obj,
402        ctx.intern("padStart"),
403        create_builtin_function(ctx, "string_padStart"),
404    );
405    set_ne(
406        &mut proto_obj,
407        ctx.intern("padEnd"),
408        create_builtin_function(ctx, "string_padEnd"),
409    );
410    set_ne(
411        &mut proto_obj,
412        ctx.intern("replaceAll"),
413        create_builtin_function(ctx, "string_replaceAll"),
414    );
415    set_ne(
416        &mut proto_obj,
417        ctx.intern("search"),
418        create_builtin_function(ctx, "string_search"),
419    );
420    set_ne(
421        &mut proto_obj,
422        ctx.intern("match"),
423        create_builtin_function(ctx, "string_match"),
424    );
425    set_ne(
426        &mut proto_obj,
427        ctx.intern("at"),
428        create_builtin_function(ctx, "string_at"),
429    );
430    set_ne(
431        &mut proto_obj,
432        ctx.intern("isWellFormed"),
433        create_builtin_function(ctx, "string_isWellFormed"),
434    );
435    set_ne(
436        &mut proto_obj,
437        ctx.intern("toWellFormed"),
438        create_builtin_function(ctx, "string_toWellFormed"),
439    );
440    set_ne(
441        &mut proto_obj,
442        ctx.intern("codePointAt"),
443        create_builtin_function(ctx, "string_codePointAt"),
444    );
445    set_ne(
446        &mut proto_obj,
447        ctx.intern("matchAll"),
448        create_builtin_function(ctx, "string_matchAll"),
449    );
450    set_ne(
451        &mut proto_obj,
452        ctx.intern("substr"),
453        create_builtin_function(ctx, "string_substr"),
454    );
455
456    if let Some(obj_proto_ptr) = ctx.get_object_prototype() {
457        proto_obj.prototype = Some(obj_proto_ptr);
458    }
459
460    let proto_ptr = Box::into_raw(Box::new(proto_obj)) as usize;
461    ctx.runtime_mut().gc_heap_mut().track(proto_ptr);
462
463    ctx.set_string_prototype(proto_ptr);
464    let proto_value = JSValue::new_object(proto_ptr);
465
466    let string_func_ptr = string_ptr as *mut crate::object::function::JSFunction;
467    unsafe {
468        (*string_func_ptr)
469            .base
470            .set(ctx.common_atoms.prototype, proto_value);
471    }
472
473    if global.is_object() {
474        let global_obj = global.as_object_mut();
475        global_obj.set(proto_atom, proto_value);
476    }
477}
478
479pub fn register_builtins(ctx: &mut JSContext) {
480    ctx.register_builtin(
481        "string_constructor",
482        HostFunction::ctor("String", 1, string_constructor),
483    );
484    ctx.register_builtin(
485        "string_charAt",
486        HostFunction::method("charAt", 1, string_char_at),
487    );
488    ctx.register_builtin(
489        "string_charCodeAt",
490        HostFunction::method("charCodeAt", 1, string_char_code_at),
491    );
492    ctx.register_builtin(
493        "string_concat",
494        HostFunction::method("concat", 1, string_concat),
495    );
496    ctx.register_builtin(
497        "string_indexOf",
498        HostFunction::method("indexOf", 1, string_index_of),
499    );
500    ctx.register_builtin(
501        "string_lastIndexOf",
502        HostFunction::method("lastIndexOf", 1, string_last_index_of),
503    );
504    ctx.register_builtin(
505        "string_localeCompare",
506        HostFunction::method("localeCompare", 1, string_locale_compare),
507    );
508    ctx.register_builtin(
509        "string_normalize",
510        HostFunction::method("normalize", 0, string_normalize),
511    );
512    ctx.register_builtin(
513        "string_slice",
514        HostFunction::method("slice", 2, string_slice),
515    );
516    ctx.register_builtin(
517        "string_substring",
518        HostFunction::method("substring", 2, string_substring),
519    );
520    ctx.register_builtin(
521        "string_toString",
522        HostFunction::method("toString", 0, string_to_string),
523    );
524    ctx.register_builtin(
525        "string_toLowerCase",
526        HostFunction::method("toLowerCase", 0, string_to_lower_case),
527    );
528    ctx.register_builtin(
529        "string_toUpperCase",
530        HostFunction::method("toUpperCase", 0, string_to_upper_case),
531    );
532    ctx.register_builtin(
533        "string_toLocaleLowerCase",
534        HostFunction::method("toLocaleLowerCase", 0, string_to_locale_lower_case),
535    );
536    ctx.register_builtin(
537        "string_toLocaleUpperCase",
538        HostFunction::method("toLocaleUpperCase", 0, string_to_locale_upper_case),
539    );
540    ctx.register_builtin(
541        "string_split",
542        HostFunction::method("split", 1, string_split),
543    );
544    ctx.register_builtin(
545        "string_length",
546        HostFunction::method("length", 0, string_length),
547    );
548    ctx.register_builtin(
549        "string_fromCharCode",
550        HostFunction::new("fromCharCode", 1, string_fromcharcode),
551    );
552    ctx.register_builtin(
553        "string_fromCodePoint",
554        HostFunction::new("fromCodePoint", 1, string_fromcodepoint),
555    );
556    ctx.register_builtin("string_raw", HostFunction::new("raw", 1, string_raw));
557    ctx.register_builtin("string_trim", HostFunction::method("trim", 0, string_trim));
558    ctx.register_builtin(
559        "string_trimStart",
560        HostFunction::method("trimStart", 0, string_trim_start),
561    );
562    ctx.register_builtin(
563        "string_trimEnd",
564        HostFunction::method("trimEnd", 0, string_trim_end),
565    );
566    ctx.register_builtin(
567        "string_startsWith",
568        HostFunction::method("startsWith", 1, string_starts_with),
569    );
570    ctx.register_builtin(
571        "string_endsWith",
572        HostFunction::method("endsWith", 1, string_ends_with),
573    );
574    ctx.register_builtin(
575        "string_repeat",
576        HostFunction::method("repeat", 1, string_repeat),
577    );
578    ctx.register_builtin(
579        "string_includes",
580        HostFunction::method("includes", 1, string_includes),
581    );
582    ctx.register_builtin(
583        "string_starts_with",
584        HostFunction::method("startsWith", 1, string_starts_with),
585    );
586    ctx.register_builtin(
587        "string_ends_with",
588        HostFunction::method("endsWith", 1, string_ends_with),
589    );
590    ctx.register_builtin(
591        "string_replace",
592        HostFunction::method("replace", 2, string_replace),
593    );
594    ctx.register_builtin(
595        "string_padStart",
596        HostFunction::method("padStart", 1, string_pad_start),
597    );
598    ctx.register_builtin(
599        "string_padEnd",
600        HostFunction::method("padEnd", 1, string_pad_end),
601    );
602    ctx.register_builtin(
603        "string_replaceAll",
604        HostFunction::method("replaceAll", 2, string_replace_all),
605    );
606    ctx.register_builtin(
607        "string_search",
608        HostFunction::method("search", 1, string_search),
609    );
610    ctx.register_builtin(
611        "string_match",
612        HostFunction::method("match", 1, string_match),
613    );
614    ctx.register_builtin("string_at", HostFunction::method("at", 1, string_at));
615    ctx.register_builtin(
616        "string_isWellFormed",
617        HostFunction::method("isWellFormed", 0, string_is_well_formed),
618    );
619    ctx.register_builtin(
620        "string_toWellFormed",
621        HostFunction::method("toWellFormed", 0, string_to_well_formed),
622    );
623    ctx.register_builtin(
624        "string_codePointAt",
625        HostFunction::method("codePointAt", 1, string_code_point_at),
626    );
627    ctx.register_builtin(
628        "string_matchAll",
629        HostFunction::method("matchAll", 1, string_match_all),
630    );
631    ctx.register_builtin(
632        "string_substr",
633        HostFunction::method("substr", 2, string_substr),
634    );
635}
636
637fn string_constructor(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
638    let s = if args.is_empty() {
639        JSValue::new_string(ctx.intern(""))
640    } else {
641        let val = &args[0];
642        if val.is_string() {
643            val.clone()
644        } else if val.is_int() {
645            JSValue::new_string(ctx.intern(&val.get_int().to_string()))
646        } else if val.is_float() {
647            JSValue::new_string(ctx.intern(&val.get_float().to_string()))
648        } else if val.is_bigint() {
649            let obj = unsafe { crate::value::JSValue::object_from_ptr(val.get_ptr()) };
650            let n = obj.get_bigint_value();
651            JSValue::new_string(ctx.intern(&n.to_string()))
652        } else if val.is_bool() {
653            JSValue::new_string(ctx.intern(&val.get_bool().to_string()))
654        } else if val.is_null() {
655            JSValue::new_string(ctx.common_atoms.null)
656        } else if val.is_undefined() {
657            JSValue::new_string(ctx.common_atoms.undefined)
658        } else if val.is_symbol() {
659            let desc_atom = val.get_atom();
660            if desc_atom == crate::builtins::symbol::NO_DESCRIPTION {
661                JSValue::new_string(ctx.intern("Symbol()"))
662            } else {
663                let desc = ctx.get_atom_str(desc_atom);
664                JSValue::new_string(ctx.intern(&format!("Symbol({})", desc)))
665            }
666        } else if val.is_object() {
667            JSValue::new_string(ctx.intern("[object Object]"))
668        } else {
669            JSValue::new_string(ctx.intern(""))
670        }
671    };
672    s
673}
674
675fn char_count(s: &str) -> usize {
676    s.chars().count()
677}
678
679fn byte_index_for_char_pos(s: &str, pos: usize) -> usize {
680    if pos == 0 {
681        return 0;
682    }
683    match s.char_indices().nth(pos) {
684        Some((idx, _)) => idx,
685        None => s.len(),
686    }
687}
688
689fn string_char_at(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
690    if args.is_empty() {
691        return JSValue::new_string(ctx.intern(""));
692    }
693    let s = match require_string_coercible(ctx, &args[0]) {
694        Some(s) => s,
695        None => return JSValue::undefined(),
696    };
697    let atom = ctx.intern(&s);
698
699    let index = if args.len() > 1 {
700        let idx = to_integer_index(&args[1], ctx);
701        if idx < 0 { 0usize } else { idx as usize }
702    } else {
703        0usize
704    };
705
706    if index >= ctx.string_char_count(atom) {
707        return JSValue::new_string(ctx.intern(""));
708    }
709
710    match ctx.string_char_code_at(atom, index) {
711        Some(code) => {
712            let c = char::from_u32(code).unwrap_or('\0');
713            let mut buf = [0u8; 4];
714            let s = c.encode_utf8(&mut buf);
715            JSValue::new_string(ctx.intern(s))
716        }
717        None => JSValue::new_string(ctx.intern("")),
718    }
719}
720
721fn string_char_code_at(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
722    if args.is_empty() {
723        return JSValue::new_float(f64::NAN);
724    }
725    let s = match require_string_coercible(ctx, &args[0]) {
726        Some(s) => s,
727        None => return JSValue::undefined(),
728    };
729    let atom = ctx.intern(&s);
730
731    let index = if args.len() > 1 {
732        let v = args[1];
733        if v.is_int() {
734            let i = v.get_int();
735            if i < 0 {
736                return JSValue::new_float(f64::NAN);
737            }
738            i as usize
739        } else if v.is_float() {
740            let f = v.get_float();
741            if f < 0.0 || f.is_nan() || f.is_infinite() {
742                return JSValue::new_float(f64::NAN);
743            }
744            f as usize
745        } else {
746            0
747        }
748    } else {
749        0
750    };
751
752    match ctx.string_char_code_at(atom, index) {
753        Some(code) => JSValue::new_int(code as i64),
754        None => JSValue::new_float(f64::NAN),
755    }
756}
757
758fn string_concat(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
759    if args.is_empty() {
760        return JSValue::new_string(ctx.intern(""));
761    }
762    let mut result = match require_string_coercible(ctx, &args[0]) {
763        Some(s) => s,
764        None => return JSValue::undefined(),
765    };
766
767    for arg in args.iter().skip(1) {
768        if arg.is_string() {
769            result.push_str(ctx.get_atom_str(arg.get_atom()));
770        } else if arg.is_int() {
771            result.push_str(&arg.get_int().to_string());
772        } else if arg.is_float() {
773            result.push_str(&arg.get_float().to_string());
774        } else if arg.is_bool() {
775            result.push_str(&arg.get_bool().to_string());
776        }
777    }
778
779    JSValue::new_string(ctx.intern(&result))
780}
781
782fn string_index_of(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
783    if args.len() < 2 {
784        return JSValue::new_int(-1);
785    }
786    let s = match require_string_coercible(ctx, &args[0]) {
787        Some(s) => s,
788        None => return JSValue::undefined(),
789    };
790
791    let search = if args[1].is_string() {
792        ctx.get_atom_str(args[1].get_atom()).to_string()
793    } else {
794        return JSValue::new_int(-1);
795    };
796
797    let from_index = if args.len() > 2 {
798        let p = &args[2];
799        let idx = if p.is_int() {
800            p.get_int()
801        } else if p.is_float() {
802            p.get_float().trunc() as i64
803        } else if p.is_undefined() || p.is_null() {
804            0
805        } else if p.is_bool() {
806            if p.get_bool() { 1 } else { 0 }
807        } else if p.is_string() {
808            let ps = ctx.get_atom_str(p.get_atom());
809            match ps.trim().parse::<f64>() {
810                Ok(v) if !v.is_nan() => v.trunc() as i64,
811                _ => 0,
812            }
813        } else {
814            0
815        };
816        if idx < 0 { 0usize } else { idx as usize }
817    } else {
818        0usize
819    };
820
821    let s_char_len = char_count(&s);
822    if from_index > s_char_len {
823        return JSValue::new_int(-1);
824    }
825
826    let from_byte = byte_index_for_char_pos(&s, from_index);
827
828    match s[from_byte..].find(&search) {
829        Some(byte_pos) => {
830            let byte_idx = from_byte + byte_pos;
831            let char_idx = s[..byte_idx].chars().count();
832            JSValue::new_int(char_idx as i64)
833        }
834        None => JSValue::new_int(-1),
835    }
836}
837
838fn string_last_index_of(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
839    if args.len() < 2 {
840        return JSValue::new_int(-1);
841    }
842    let s = match require_string_coercible(ctx, &args[0]) {
843        Some(s) => s,
844        None => return JSValue::undefined(),
845    };
846
847    let search = if args[1].is_string() {
848        ctx.get_atom_str(args[1].get_atom()).to_string()
849    } else {
850        return JSValue::new_int(-1);
851    };
852
853    match s.rfind(&search) {
854        Some(byte_pos) => {
855            let char_idx = s[..byte_pos].chars().count();
856            JSValue::new_int(char_idx as i64)
857        }
858        None => JSValue::new_int(-1),
859    }
860}
861
862fn string_locale_compare(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
863    let this_s = match require_string_coercible(ctx, &args[0]) {
864        Some(s) => s,
865        None => return JSValue::undefined(),
866    };
867    let cmp_s = if args.len() > 1 {
868        match require_string_coercible(ctx, &args[1]) {
869            Some(s) => s,
870            None => return JSValue::undefined(),
871        }
872    } else {
873        "undefined".to_string()
874    };
875    let res = match this_s.cmp(&cmp_s) {
876        std::cmp::Ordering::Less => -1,
877        std::cmp::Ordering::Equal => 0,
878        std::cmp::Ordering::Greater => 1,
879    };
880    JSValue::new_int(res)
881}
882
883fn string_normalize(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
884    let this_s = match require_string_coercible(ctx, &args[0]) {
885        Some(s) => s,
886        None => return JSValue::undefined(),
887    };
888    if args.len() > 1 && !args[1].is_undefined() {
889        let form_str = match require_string_coercible(ctx, &args[1]) {
890            Some(s) => s,
891            None => return JSValue::undefined(),
892        };
893        let valid = matches!(form_str.as_str(), "NFC" | "NFD" | "NFKC" | "NFKD");
894        if !valid {
895            let mut err = JSObject::new();
896            err.set(
897                ctx.common_atoms.name,
898                JSValue::new_string(ctx.intern("RangeError")),
899            );
900            err.set(
901                ctx.common_atoms.message,
902                JSValue::new_string(ctx.intern("Normalization form is invalid")),
903            );
904            if let Some(proto) = ctx.get_range_error_prototype() {
905                err.prototype = Some(proto);
906            }
907            let ptr = Box::into_raw(Box::new(err)) as usize;
908            ctx.runtime_mut().gc_heap_mut().track(ptr);
909            ctx.pending_exception = Some(JSValue::new_object(ptr));
910            return JSValue::undefined();
911        }
912    }
913    JSValue::new_string(ctx.intern(&this_s))
914}
915
916fn string_substring(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
917    if args.is_empty() {
918        return JSValue::new_string(ctx.intern(""));
919    }
920    let s = match require_string_coercible(ctx, &args[0]) {
921        Some(s) => s,
922        None => return JSValue::undefined(),
923    };
924
925    let len = char_count(&s);
926    let start = if args.len() > 1 {
927        let v = to_integer_index(&args[1], ctx).max(0) as usize;
928        v.min(len)
929    } else {
930        0
931    };
932
933    let end = if args.len() > 2 {
934        let v = to_integer_index(&args[2], ctx).max(0) as usize;
935        v.min(len)
936    } else {
937        len
938    };
939
940    let (start, end) = if start > end {
941        (end, start)
942    } else {
943        (start, end)
944    };
945
946    let start_b = byte_index_for_char_pos(&s, start);
947    let end_b = byte_index_for_char_pos(&s, end);
948    JSValue::new_string(ctx.intern(&s[start_b..end_b]))
949}
950
951fn string_slice(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
952    if args.is_empty() {
953        return JSValue::new_string(ctx.intern(""));
954    }
955    let s = match require_string_coercible(ctx, &args[0]) {
956        Some(s) => s,
957        None => return JSValue::undefined(),
958    };
959
960    let len = char_count(&s);
961    let start = if args.len() > 1 {
962        let v = to_integer_index(&args[1], ctx);
963        if v < 0 {
964            (len as i64 + v).max(0) as usize
965        } else {
966            (v as usize).min(len)
967        }
968    } else {
969        0
970    };
971
972    let end = if args.len() > 2 {
973        let v = to_integer_index(&args[2], ctx);
974        if v < 0 {
975            (len as i64 + v).max(0) as usize
976        } else {
977            (v as usize).min(len)
978        }
979    } else {
980        len
981    };
982
983    if start >= end {
984        return JSValue::new_string(ctx.intern(""));
985    }
986
987    let start_b = byte_index_for_char_pos(&s, start);
988    let end_b = byte_index_for_char_pos(&s, end);
989    JSValue::new_string(ctx.intern(&s[start_b..end_b]))
990}
991
992fn string_to_string(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
993    if args.is_empty() {
994        return JSValue::new_string(ctx.intern(""));
995    }
996    match require_string_coercible(ctx, &args[0]) {
997        Some(s) => JSValue::new_string(ctx.intern(&s)),
998        None => JSValue::undefined(),
999    }
1000}
1001
1002fn string_to_lower_case(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
1003    if args.is_empty() {
1004        return JSValue::new_string(ctx.intern(""));
1005    }
1006    let s = match require_string_coercible(ctx, &args[0]) {
1007        Some(s) => s.to_lowercase(),
1008        None => return JSValue::undefined(),
1009    };
1010
1011    JSValue::new_string(ctx.intern(&s))
1012}
1013
1014fn string_to_upper_case(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
1015    if args.is_empty() {
1016        return JSValue::new_string(ctx.intern(""));
1017    }
1018    let s = match require_string_coercible(ctx, &args[0]) {
1019        Some(s) => s.to_uppercase(),
1020        None => return JSValue::undefined(),
1021    };
1022
1023    JSValue::new_string(ctx.intern(&s))
1024}
1025
1026fn string_to_locale_lower_case(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
1027    string_to_lower_case(ctx, args)
1028}
1029
1030fn string_to_locale_upper_case(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
1031    string_to_upper_case(ctx, args)
1032}
1033
1034fn string_split(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
1035    if args.is_empty() {
1036        let mut result = JSObject::new_array();
1037    if let Some(p) = ctx.get_array_prototype() {
1038        result.set_prototype_raw(p);
1039    }
1040        result.set(ctx.common_atoms.length, JSValue::new_int(0));
1041        let ptr = Box::into_raw(Box::new(result)) as usize;
1042        return JSValue::new_object(ptr);
1043    }
1044    let s = match require_string_coercible(ctx, &args[0]) {
1045        Some(s) => s,
1046        None => return JSValue::undefined(),
1047    };
1048    let s_atom = ctx.intern(&s);
1049
1050    let length_atom = ctx.common_atoms.length;
1051
1052    if args.len() > 1 && args[1].is_object() {
1053        let sep_obj = args[1].as_object();
1054        let pattern_atom = ctx.common_atoms.__pattern__;
1055        if sep_obj.get(pattern_atom).is_some() {
1056            let limit = if args.len() > 2 && args[2].is_int() {
1057                args[2].get_int() as usize
1058            } else {
1059                usize::MAX
1060            };
1061
1062            let mut parts: Vec<String> = Vec::new();
1063            let compiled_re = args[1]
1064                .as_object()
1065                .get_compiled_regex()
1066                .map(|re| re as *const crate::regexp::Regex);
1067            if let Some(re_ptr) = compiled_re {
1068                let re = unsafe { &*re_ptr };
1069                let mut last = 0usize;
1070                let mut search_start = 0usize;
1071                loop {
1072                    if parts.len() >= limit {
1073                        break;
1074                    }
1075                    if search_start > s.len() {
1076                        break;
1077                    }
1078                    let sub = &s[search_start..];
1079                    if let Some(m) = re.find(sub) {
1080                        let match_start = search_start + m.start();
1081                        let match_end = search_start + m.end();
1082                        if match_end == search_start && match_start == last {
1083                            search_start += s[search_start..]
1084                                .chars()
1085                                .next()
1086                                .map(|c| c.len_utf8())
1087                                .unwrap_or(1);
1088                            continue;
1089                        }
1090                        parts.push(s[last..match_start].to_string());
1091
1092                        for cap in m.iter().skip(1) {
1093                            parts.push(cap.unwrap_or("").to_string());
1094                        }
1095                        last = match_end;
1096                        search_start = match_end;
1097                        if match_end == match_start {
1098                            search_start += s[search_start..]
1099                                .chars()
1100                                .next()
1101                                .map(|c| c.len_utf8())
1102                                .unwrap_or(1);
1103                        }
1104                    } else {
1105                        break;
1106                    }
1107                }
1108                if parts.len() < limit {
1109                    parts.push(s[last..].to_string());
1110                }
1111            } else {
1112                parts.push(s);
1113            }
1114
1115            let mut result = JSObject::new_array();
1116    if let Some(p) = ctx.get_array_prototype() {
1117        result.set_prototype_raw(p);
1118    }
1119            for (i, part) in parts.iter().enumerate() {
1120                let key = ctx.int_atom_mut(i);
1121                let part_atom = ctx.intern(part);
1122                result.set(key, JSValue::new_string(part_atom));
1123            }
1124            result.set(length_atom, JSValue::new_int(parts.len() as i64));
1125            let ptr = Box::into_raw(Box::new(result)) as usize;
1126            return JSValue::new_object(ptr);
1127        }
1128    }
1129
1130    let sep_atom = if args.len() > 1 && args[1].is_string() {
1131        Some(args[1].get_atom())
1132    } else if args.len() > 1 && !args[1].is_undefined() {
1133        let s = match this_to_string(ctx, &args[1]) {
1134            Some(s) => s,
1135            None => return JSValue::undefined(),
1136        };
1137        Some(ctx.intern(&s))
1138    } else {
1139        None
1140    };
1141
1142    let s_owned = ctx.get_atom_str(s_atom).to_string();
1143    let s = s_owned.as_str();
1144
1145    if args.len() <= 1 || args[1].is_undefined() {
1146        let limit = if args.len() > 2 && !args[2].is_undefined() {
1147            to_integer_index(&args[2], ctx).max(0) as usize
1148        } else {
1149            usize::MAX
1150        };
1151        if limit == 0 {
1152            let mut result = JSObject::new_array();
1153    if let Some(p) = ctx.get_array_prototype() {
1154        result.set_prototype_raw(p);
1155    }
1156            result.set(length_atom, JSValue::new_int(0));
1157            let ptr = Box::into_raw(Box::new(result)) as usize;
1158            return JSValue::new_object(ptr);
1159        }
1160        let mut result = JSObject::new_array();
1161    if let Some(p) = ctx.get_array_prototype() {
1162        result.set_prototype_raw(p);
1163    }
1164        result.set(ctx.int_atom_mut(0), JSValue::new_string(s_atom));
1165        result.set(length_atom, JSValue::new_int(1));
1166        let ptr = Box::into_raw(Box::new(result)) as usize;
1167        return JSValue::new_object(ptr);
1168    }
1169
1170    let limit = if args.len() > 2 && !args[2].is_undefined() {
1171        to_integer_index(&args[2], ctx).max(0) as usize
1172    } else {
1173        usize::MAX
1174    };
1175
1176    let separator = sep_atom
1177        .map(|a| ctx.get_atom_str(a).to_string())
1178        .unwrap_or_default();
1179    let s = s.to_string();
1180
1181    if limit == 0 {
1182        let mut result = JSObject::new_array();
1183    if let Some(p) = ctx.get_array_prototype() {
1184        result.set_prototype_raw(p);
1185    }
1186        result.set(length_atom, JSValue::new_int(0));
1187        let ptr = Box::into_raw(Box::new(result)) as usize;
1188        return JSValue::new_object(ptr);
1189    }
1190
1191    let mut result = JSObject::new_array();
1192    if let Some(p) = ctx.get_array_prototype() {
1193        result.set_prototype_raw(p);
1194    }
1195
1196    if separator.is_empty() {
1197        let chars: Vec<char> = s.chars().collect();
1198        let count = chars.len().min(limit);
1199        for i in 0..count {
1200            let key = ctx.int_atom_mut(i);
1201            let c_str = chars[i].to_string();
1202            let c_atom = ctx.intern(&c_str);
1203            result.set(key, JSValue::new_string(c_atom));
1204        }
1205        result.set(length_atom, JSValue::new_int(count as i64));
1206    } else {
1207        let parts: Vec<String> = s.split(separator.as_str()).map(|s| s.to_string()).collect();
1208        let count = parts.len().min(limit);
1209        for i in 0..count {
1210            let key = ctx.int_atom_mut(i);
1211            let part_atom = ctx.intern(&parts[i]);
1212            result.set(key, JSValue::new_string(part_atom));
1213        }
1214        result.set(length_atom, JSValue::new_int(count as i64));
1215    }
1216
1217    let ptr = Box::into_raw(Box::new(result)) as usize;
1218    JSValue::new_object(ptr)
1219}
1220
1221fn string_length(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
1222    if args.is_empty() {
1223        return JSValue::new_int(0);
1224    }
1225    let s = match require_string_coercible(ctx, &args[0]) {
1226        Some(s) => s,
1227        None => return JSValue::undefined(),
1228    };
1229    JSValue::new_int(char_count(&s) as i64)
1230}
1231
1232fn from_str_or_hex(s: &str) -> u32 {
1233    let s = s.trim();
1234    if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
1235        u32::from_str_radix(hex, 16).unwrap_or(0)
1236    } else {
1237        s.parse::<f64>().unwrap_or(0.0) as u32
1238    }
1239}
1240
1241fn string_fromcharcode(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
1242    let mut units: Vec<u16> = Vec::with_capacity(args.len());
1243    for arg in args {
1244        let n = match crate::builtins::global::js_to_number_value(ctx, arg) {
1245            Ok(n) => n,
1246            Err(()) => return JSValue::undefined(),
1247        };
1248        let unit = to_uint16(n);
1249        units.push(unit);
1250    }
1251    let result: String = String::from_utf16_lossy(&units);
1252    JSValue::new_string(ctx.intern(&result))
1253}
1254
1255fn to_uint16(n: f64) -> u16 {
1256    if n.is_nan() || n.is_infinite() {
1257        return 0;
1258    }
1259    let i = n.trunc();
1260    let modulo = ((i.round() as i128).rem_euclid(65536)) as u16;
1261    modulo
1262}
1263
1264fn string_fromcodepoint(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
1265    let mut result = String::new();
1266    for arg in args {
1267        let cp = if arg.is_int() {
1268            arg.get_int() as f64
1269        } else if arg.is_float() {
1270            arg.get_float()
1271        } else if arg.is_undefined() || arg.is_null() {
1272            0.0
1273        } else if arg.is_bool() {
1274            if arg.get_bool() { 1.0 } else { 0.0 }
1275        } else {
1276            let mut err = crate::object::object::JSObject::new();
1277            err.set(
1278                ctx.common_atoms.name,
1279                JSValue::new_string(ctx.intern("TypeError")),
1280            );
1281            err.set(
1282                ctx.common_atoms.message,
1283                JSValue::new_string(ctx.intern("Invalid code point")),
1284            );
1285            if let Some(proto) = ctx.get_type_error_prototype() {
1286                err.prototype = Some(proto);
1287            }
1288            let ptr = Box::into_raw(Box::new(err)) as usize;
1289            ctx.runtime_mut().gc_heap_mut().track(ptr);
1290            ctx.pending_exception = Some(JSValue::new_object(ptr));
1291            return JSValue::undefined();
1292        };
1293        if cp.is_nan() || cp < 0.0 || cp > 1114111.0 || cp != cp.floor() {
1294            let mut err = crate::object::object::JSObject::new();
1295            err.set(
1296                ctx.common_atoms.name,
1297                JSValue::new_string(ctx.intern("RangeError")),
1298            );
1299            err.set(
1300                ctx.common_atoms.message,
1301                JSValue::new_string(ctx.intern("Invalid code point")),
1302            );
1303            if let Some(proto) = ctx.get_range_error_prototype() {
1304                err.prototype = Some(proto);
1305            }
1306            let ptr = Box::into_raw(Box::new(err)) as usize;
1307            ctx.runtime_mut().gc_heap_mut().track(ptr);
1308            ctx.pending_exception = Some(JSValue::new_object(ptr));
1309            return JSValue::undefined();
1310        }
1311        let code = cp as u32;
1312        if let Some(c) = char::from_u32(code) {
1313            result.push(c);
1314        } else if code >= 0xD800 && code <= 0xDFFF {
1315            result.push('\u{FFFD}');
1316        } else {
1317            result.push('\u{FFFD}');
1318        }
1319    }
1320    JSValue::new_string(ctx.intern(&result))
1321}
1322
1323fn string_trim(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
1324    if args.is_empty() {
1325        return JSValue::new_string(ctx.intern(""));
1326    }
1327    let s = match require_string_coercible(ctx, &args[0]) {
1328        Some(s) => s,
1329        None => return JSValue::undefined(),
1330    };
1331    let trimmed = s.trim_matches(is_js_trim_whitespace);
1332    JSValue::new_string(ctx.intern(trimmed))
1333}
1334
1335#[inline]
1336fn is_js_trim_whitespace(c: char) -> bool {
1337    matches!(
1338        c,
1339        '\u{0009}' | '\u{000A}' | '\u{000B}' | '\u{000C}' | '\u{000D}' | '\u{0020}'
1340            | '\u{00A0}' | '\u{FEFF}' | '\u{1680}' | '\u{2028}' | '\u{2029}' | '\u{202F}'
1341            | '\u{205F}' | '\u{3000}' | '\u{2000}'..='\u{200A}'
1342    )
1343}
1344
1345fn string_trim_start(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
1346    if args.is_empty() {
1347        return JSValue::new_string(ctx.intern(""));
1348    }
1349    match this_to_string(ctx, &args[0]) {
1350        Some(s) => JSValue::new_string(ctx.intern(s.trim_start_matches(is_js_trim_whitespace))),
1351        None => JSValue::undefined(),
1352    }
1353}
1354
1355fn string_trim_end(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
1356    if args.is_empty() {
1357        return JSValue::new_string(ctx.intern(""));
1358    }
1359    match this_to_string(ctx, &args[0]) {
1360        Some(s) => JSValue::new_string(ctx.intern(s.trim_end_matches(is_js_trim_whitespace))),
1361        None => JSValue::undefined(),
1362    }
1363}
1364
1365fn string_starts_with(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
1366    let s = match this_to_string(ctx, &args[0]) {
1367        Some(s) => s,
1368        None => return JSValue::undefined(),
1369    };
1370    let search = if args.len() > 1 {
1371        ctx.get_atom_str(args[1].get_atom()).to_string()
1372    } else {
1373        "undefined".to_string()
1374    };
1375    if search.is_empty() {
1376        return JSValue::bool(true);
1377    }
1378    let pos = if args.len() > 2 {
1379        let idx = to_integer_index(&args[2], ctx);
1380        if idx < 0 { 0usize } else { idx as usize }
1381    } else {
1382        0usize
1383    };
1384    if pos > s.len() {
1385        return JSValue::bool(false);
1386    }
1387    JSValue::bool(s[pos..].starts_with(&search))
1388}
1389
1390fn string_ends_with(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
1391    let s = match this_to_string(ctx, &args[0]) {
1392        Some(s) => s,
1393        None => return JSValue::undefined(),
1394    };
1395    let search = if args.len() > 1 {
1396        ctx.get_atom_str(args[1].get_atom()).to_string()
1397    } else {
1398        "undefined".to_string()
1399    };
1400    if search.is_empty() {
1401        return JSValue::bool(true);
1402    }
1403    let len = s.len();
1404    let end_pos = if args.len() > 2 {
1405        let idx = to_integer_index(&args[2], ctx);
1406        if idx < 0 {
1407            0usize
1408        } else {
1409            (idx as usize).min(len)
1410        }
1411    } else {
1412        len
1413    };
1414    JSValue::bool(s[..end_pos].ends_with(&search))
1415}
1416
1417fn string_repeat(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
1418    if args.is_empty() {
1419        return JSValue::new_string(ctx.intern(""));
1420    }
1421    let s = match this_to_string(ctx, &args[0]) {
1422        Some(s) => s,
1423        None => return JSValue::undefined(),
1424    };
1425    if args.len() < 2 {
1426        return JSValue::new_string(ctx.intern(""));
1427    }
1428    let count = to_integer_index(&args[1], ctx);
1429    if count < 0 {
1430        return JSValue::new_string(ctx.intern(""));
1431    }
1432    let count = count as usize;
1433    let repeated = s.repeat(count);
1434    JSValue::new_string(ctx.intern(&repeated))
1435}
1436
1437fn string_includes(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
1438    let s = match this_to_string(ctx, &args[0]) {
1439        Some(s) => s,
1440        None => return JSValue::undefined(),
1441    };
1442    if args.len() < 2 {
1443        return JSValue::bool(s.contains("undefined"));
1444    }
1445    let search = ctx.get_atom_str(args[1].get_atom()).to_string();
1446    let pos = if args.len() > 2 {
1447        let idx = to_integer_index(&args[2], ctx);
1448        if idx < 0 { 0usize } else { idx as usize }
1449    } else {
1450        0usize
1451    };
1452    if pos > s.len() {
1453        return JSValue::bool(false);
1454    }
1455    JSValue::bool(s[pos..].contains(&search))
1456}
1457
1458fn string_replace(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
1459    if args.is_empty() {
1460        return JSValue::new_string(ctx.intern(""));
1461    }
1462    let s = match require_string_coercible(ctx, &args[0]) {
1463        Some(s) => s,
1464        None => return JSValue::undefined(),
1465    };
1466    if args.len() < 3 {
1467        return JSValue::new_string(ctx.intern(&s));
1468    }
1469
1470    if args[1].is_object() {
1471        let regexp_obj = args[1].as_object();
1472
1473        let pattern_atom = ctx.common_atoms.__pattern__;
1474        let flags_atom = ctx.common_atoms.__flags__;
1475
1476        let pattern = if let Some(p) = regexp_obj.get(pattern_atom) {
1477            if p.is_string() {
1478                ctx.get_atom_str(p.get_atom()).to_string()
1479            } else {
1480                return JSValue::new_string(ctx.intern(&s));
1481            }
1482        } else {
1483            return JSValue::new_string(ctx.intern(&s));
1484        };
1485
1486        let flags = if let Some(f) = regexp_obj.get(flags_atom) {
1487            if f.is_string() {
1488                ctx.get_atom_str(f.get_atom()).to_string()
1489            } else {
1490                String::new()
1491            }
1492        } else {
1493            String::new()
1494        };
1495
1496        let replacement = if args[2].is_string() {
1497            ctx.get_atom_str(args[2].get_atom()).to_string()
1498        } else {
1499            String::new()
1500        };
1501
1502        let ignore_case = flags.contains('i');
1503        let is_global = flags.contains('g');
1504
1505        if is_global {
1506            let mut result = String::new();
1507            let mut last_end = 0;
1508            let mut search_from = 0;
1509            let pat_lower = if ignore_case {
1510                pattern.to_lowercase()
1511            } else {
1512                String::new()
1513            };
1514            let s_lower = if ignore_case {
1515                s.to_lowercase()
1516            } else {
1517                String::new()
1518            };
1519
1520            loop {
1521                let found = if ignore_case {
1522                    s_lower[search_from..]
1523                        .find(&pat_lower)
1524                        .map(|i| i + search_from)
1525                } else {
1526                    s[search_from..].find(&pattern).map(|i| i + search_from)
1527                };
1528
1529                if let Some(pos) = found {
1530                    result.push_str(&s[last_end..pos]);
1531                    result.push_str(&replacement);
1532                    last_end = pos + pattern.len();
1533                    search_from = last_end;
1534                    if pattern.is_empty() {
1535                        if search_from < s.len() {
1536                            result.push_str(&s[search_from..search_from + 1]);
1537                            search_from += 1;
1538                            last_end = search_from;
1539                        } else {
1540                            break;
1541                        }
1542                    }
1543                } else {
1544                    break;
1545                }
1546            }
1547            result.push_str(&s[last_end..]);
1548            JSValue::new_string(ctx.intern(&result))
1549        } else {
1550            let found = if ignore_case {
1551                s.to_lowercase().find(&pattern.to_lowercase())
1552            } else {
1553                s.find(&pattern)
1554            };
1555
1556            if let Some(pos) = found {
1557                let mut result = String::new();
1558                result.push_str(&s[..pos]);
1559                result.push_str(&replacement);
1560                result.push_str(&s[pos + pattern.len()..]);
1561                JSValue::new_string(ctx.intern(&result))
1562            } else {
1563                JSValue::new_string(ctx.intern(&s))
1564            }
1565        }
1566    } else {
1567        let search = if args[1].is_string() {
1568            ctx.get_atom_str(args[1].get_atom()).to_string()
1569        } else {
1570            return JSValue::new_string(ctx.intern(&s));
1571        };
1572        match s.find(&search) {
1573            None => JSValue::new_string(ctx.intern(&s)),
1574            Some(pos) => {
1575                let end = pos + search.len();
1576                let matched = search.clone();
1577                let repl_str = if args[2].is_function() {
1578                    call_replace_function(ctx, args[2], &matched, pos, &s, &[])
1579                } else if args[2].is_string() {
1580                    let r = ctx.get_atom_str(args[2].get_atom()).to_string();
1581                    apply_replace_pattern(&r, &matched, pos, end, &s)
1582                } else {
1583                    String::new()
1584                };
1585                let mut result = String::with_capacity(s.len() + repl_str.len());
1586                result.push_str(&s[..pos]);
1587                result.push_str(&repl_str);
1588                result.push_str(&s[end..]);
1589                JSValue::new_string(ctx.intern(&result))
1590            }
1591        }
1592    }
1593}
1594
1595fn call_replace_function(
1596    ctx: &mut JSContext,
1597    func: JSValue,
1598    matched: &str,
1599    position: usize,
1600    full: &str,
1601    captures: &[&str],
1602) -> String {
1603    let mut args: Vec<JSValue> = Vec::with_capacity(3 + captures.len());
1604    args.push(JSValue::new_string(ctx.intern(matched)));
1605    for c in captures {
1606        args.push(JSValue::new_string(ctx.intern(c)));
1607    }
1608    args.push(JSValue::new_int(position as i64));
1609    args.push(JSValue::new_string(ctx.intern(full)));
1610    let val = if let Some(vm_ptr) = ctx.get_register_vm_ptr() {
1611        let vm = unsafe { &mut *(vm_ptr as *mut crate::runtime::vm::VM) };
1612        match vm.call_function_with_this(ctx, func, JSValue::undefined(), &args) {
1613            Ok(v) => v,
1614            Err(_) => JSValue::undefined(),
1615        }
1616    } else {
1617        JSValue::undefined()
1618    };
1619    js_to_string_arg(&val, ctx)
1620}
1621
1622fn apply_replace_pattern(replacement: &str, matched: &str, start: usize, end: usize, full: &str) -> String {
1623    let bytes = replacement.as_bytes();
1624    let mut out = String::with_capacity(replacement.len());
1625    let mut i = 0;
1626    while i < bytes.len() {
1627        if bytes[i] == b'$' && i + 1 < bytes.len() {
1628            let d = bytes[i + 1];
1629            match d {
1630                b'$' => {
1631                    out.push('$');
1632                    i += 2;
1633                    continue;
1634                }
1635                b'&' => {
1636                    out.push_str(matched);
1637                    i += 2;
1638                    continue;
1639                }
1640                b'`' => {
1641                    out.push_str(&full[..start]);
1642                    i += 2;
1643                    continue;
1644                }
1645                b'\'' => {
1646                    out.push_str(&full[end..]);
1647                    i += 2;
1648                    continue;
1649                }
1650                n @ (b'1'..=b'9') => {
1651                    // capture groups unsupported for plain-string replace -> empty
1652                    let _ = n;
1653                    i += 2;
1654                    continue;
1655                }
1656                _ => {}
1657            }
1658        }
1659        // copy one UTF-8 char
1660        let ch_len = utf8_len(bytes[i]);
1661        out.push_str(std::str::from_utf8(&bytes[i..i + ch_len]).unwrap_or(""));
1662        i += ch_len;
1663    }
1664    out
1665}
1666
1667#[inline]
1668fn utf8_len(b: u8) -> usize {
1669    if b < 0x80 {
1670        1
1671    } else if b >> 5 == 0b110 {
1672        2
1673    } else if b >> 4 == 0b1110 {
1674        3
1675    } else {
1676        4
1677    }
1678}
1679
1680pub fn js_to_length(val: &JSValue) -> u64 {
1681    let n = val.to_number();
1682    if n.is_nan() || n <= 0.0 {
1683        0
1684    } else if n.is_infinite() {
1685        u64::MAX
1686    } else {
1687        n as u64
1688    }
1689}
1690
1691fn js_to_string_arg(val: &JSValue, ctx: &mut JSContext) -> String {
1692    if val.is_string() {
1693        ctx.get_atom_str(val.get_atom()).to_string()
1694    } else if val.is_int() {
1695        format!("{}", val.get_int())
1696    } else if val.is_float() {
1697        js_float_to_string(val.get_float())
1698    } else if val.is_bool() {
1699        if val.get_bool() {
1700            "true".to_string()
1701        } else {
1702            "false".to_string()
1703        }
1704    } else if val.is_undefined() {
1705        "undefined".to_string()
1706    } else if val.is_null() {
1707        "null".to_string()
1708    } else {
1709        "[object Object]".to_string()
1710    }
1711}
1712
1713fn string_pad_start(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
1714    if args.is_empty() {
1715        return JSValue::new_string(ctx.intern(""));
1716    }
1717    let s = match require_string_coercible(ctx, &args[0]) {
1718        Some(s) => s,
1719        None => return JSValue::undefined(),
1720    };
1721    let target_len = if args.len() > 1 {
1722        js_to_length(&args[1]).min(1 << 30) as usize
1723    } else {
1724        0
1725    };
1726    let pad_str = if args.len() > 2 {
1727        js_to_string_arg(&args[2], ctx)
1728    } else {
1729        " ".to_string()
1730    };
1731
1732    let s_len = s.chars().count();
1733    if target_len <= s_len {
1734        return JSValue::new_string(ctx.intern(&s));
1735    }
1736
1737    let mut pad_count = target_len - s_len;
1738    let mut prepend = String::new();
1739    let pad_chars: Vec<char> = pad_str.chars().collect();
1740    let pad_char_len = pad_chars.len();
1741    if pad_char_len == 0 {
1742        return JSValue::new_string(ctx.intern(&s));
1743    }
1744    let mut i = 0;
1745    while pad_count > 0 {
1746        prepend.push(pad_chars[i % pad_char_len]);
1747        pad_count -= 1;
1748        i += 1;
1749    }
1750
1751    JSValue::new_string(ctx.intern(&(prepend + &s)))
1752}
1753
1754fn string_pad_end(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
1755    if args.is_empty() {
1756        return JSValue::new_string(ctx.intern(""));
1757    }
1758    let s = match require_string_coercible(ctx, &args[0]) {
1759        Some(s) => s,
1760        None => return JSValue::undefined(),
1761    };
1762    let target_len = if args.len() > 1 {
1763        js_to_length(&args[1]).min(1 << 30) as usize
1764    } else {
1765        0
1766    };
1767    let pad_str = if args.len() > 2 {
1768        js_to_string_arg(&args[2], ctx)
1769    } else {
1770        " ".to_string()
1771    };
1772
1773    let s_len = s.chars().count();
1774    if target_len <= s_len {
1775        return JSValue::new_string(ctx.intern(&s));
1776    }
1777
1778    let mut pad_count = target_len - s_len;
1779    let mut append = String::new();
1780    let pad_chars: Vec<char> = pad_str.chars().collect();
1781    let pad_char_len = pad_chars.len();
1782    if pad_char_len == 0 {
1783        return JSValue::new_string(ctx.intern(&s));
1784    }
1785    let mut i = 0;
1786    while pad_count > 0 {
1787        append.push(pad_chars[i % pad_char_len]);
1788        pad_count -= 1;
1789        i += 1;
1790    }
1791
1792    JSValue::new_string(ctx.intern(&(s + &append)))
1793}
1794
1795fn string_replace_all(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
1796    if args.is_empty() {
1797        return JSValue::new_string(ctx.intern(""));
1798    }
1799    let s = match require_string_coercible(ctx, &args[0]) {
1800        Some(s) => s,
1801        None => return JSValue::undefined(),
1802    };
1803    if args.len() < 3 {
1804        return JSValue::new_string(ctx.intern(&s));
1805    }
1806    let search = if args[1].is_string() {
1807        ctx.get_atom_str(args[1].get_atom()).to_string()
1808    } else {
1809        return args[0];
1810    };
1811    let replacement = if args[2].is_string() {
1812        ctx.get_atom_str(args[2].get_atom()).to_string()
1813    } else {
1814        String::new()
1815    };
1816
1817    if search.is_empty() {
1818        let mut result = String::new();
1819        for c in s.chars() {
1820            result.push_str(&replacement);
1821            result.push(c);
1822        }
1823        result.push_str(&replacement);
1824        return JSValue::new_string(ctx.intern(&result));
1825    }
1826
1827    let result = s.replace(&search as &str, &replacement as &str);
1828    JSValue::new_string(ctx.intern(&result))
1829}
1830
1831fn string_at(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
1832    if args.is_empty() {
1833        return JSValue::undefined();
1834    }
1835    let s = match require_string_coercible(ctx, &args[0]) {
1836        Some(s) => s,
1837        None => return JSValue::undefined(),
1838    };
1839    let chars: Vec<char> = s.chars().collect();
1840    let len = chars.len();
1841    let idx = if args.len() > 1 {
1842        let pos = &args[1];
1843        if pos.is_int() {
1844            pos.get_int()
1845        } else if pos.is_float() {
1846            pos.get_float().trunc() as i64
1847        } else if pos.is_undefined() || pos.is_null() {
1848            0
1849        } else if pos.is_bool() {
1850            if pos.get_bool() { 1 } else { 0 }
1851        } else if pos.is_string() {
1852            let s = ctx.get_atom_str(pos.get_atom());
1853            match s.trim().parse::<f64>() {
1854                Ok(v) if !v.is_nan() => v.trunc() as i64,
1855                _ => 0,
1856            }
1857        } else {
1858            0
1859        }
1860    } else {
1861        0
1862    };
1863    let actual_idx = if idx < 0 { len as i64 + idx } else { idx };
1864    if actual_idx < 0 || actual_idx as usize >= len {
1865        return JSValue::undefined();
1866    }
1867    let c = chars[actual_idx as usize];
1868    JSValue::new_string(ctx.intern(&c.to_string()))
1869}
1870
1871fn string_is_well_formed(_ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
1872    if args.is_empty() {
1873        return JSValue::bool(false);
1874    }
1875    let s = match require_string_coercible(_ctx, &args[0]) {
1876        Some(s) => s,
1877        None => return JSValue::undefined(),
1878    };
1879    let units: Vec<u16> = s.encode_utf16().collect();
1880    let mut i = 0;
1881    while i < units.len() {
1882        let unit = units[i];
1883        if (0xD800..=0xDBFF).contains(&unit) {
1884            if i + 1 < units.len() && (0xDC00..=0xDFFF).contains(&units[i + 1]) {
1885                i += 2;
1886                continue;
1887            }
1888            return JSValue::bool(false);
1889        }
1890        if (0xDC00..=0xDFFF).contains(&unit) {
1891            return JSValue::bool(false);
1892        }
1893        i += 1;
1894    }
1895    JSValue::bool(true)
1896}
1897
1898fn string_to_well_formed(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
1899    if args.is_empty() {
1900        return JSValue::undefined();
1901    }
1902    let s = match require_string_coercible(ctx, &args[0]) {
1903        Some(s) => s,
1904        None => return JSValue::undefined(),
1905    };
1906    let units: Vec<u16> = s.encode_utf16().collect();
1907    let mut result: Vec<u16> = Vec::with_capacity(units.len());
1908    let mut i = 0;
1909    while i < units.len() {
1910        let unit = units[i];
1911        if (0xD800..=0xDBFF).contains(&unit) {
1912            if i + 1 < units.len() && (0xDC00..=0xDFFF).contains(&units[i + 1]) {
1913                result.push(unit);
1914                result.push(units[i + 1]);
1915                i += 2;
1916                continue;
1917            }
1918            result.push(0xFFFD);
1919            i += 1;
1920            continue;
1921        }
1922        if (0xDC00..=0xDFFF).contains(&unit) {
1923            result.push(0xFFFD);
1924            i += 1;
1925            continue;
1926        }
1927        result.push(unit);
1928        i += 1;
1929    }
1930    let formatted: String = String::from_utf16_lossy(&result);
1931    JSValue::new_string(ctx.intern(&formatted))
1932}
1933
1934fn string_code_point_at(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
1935    if args.is_empty() {
1936        return JSValue::undefined();
1937    }
1938    let s = match require_string_coercible(ctx, &args[0]) {
1939        Some(s) => s,
1940        None => return JSValue::undefined(),
1941    };
1942    let chars: Vec<char> = s.chars().collect();
1943    let len = chars.len();
1944    let index = if args.len() > 1 {
1945        to_integer_index(&args[1], ctx)
1946    } else {
1947        0
1948    };
1949    let actual_index = if index < 0 { len as i64 + index } else { index };
1950    if actual_index < 0 || actual_index as usize >= len {
1951        return JSValue::undefined();
1952    }
1953    let c = chars[actual_index as usize];
1954    if (c as u32) >= 0xD800 && (c as u32) <= 0xDBFF && actual_index as usize + 1 < len {
1955        let next = chars[actual_index as usize + 1];
1956        if (next as u32) >= 0xDC00 && (next as u32) <= 0xDFFF {
1957            let cp = 0x10000 + (((c as u32) - 0xD800) << 10) + (next as u32) - 0xDC00;
1958            return JSValue::new_int(cp as i64);
1959        }
1960    }
1961    JSValue::new_int(c as u32 as i64)
1962}
1963
1964fn string_match_all(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
1965    if args.is_empty() {
1966        let mut result = JSObject::new_array();
1967    if let Some(p) = ctx.get_array_prototype() {
1968        result.set_prototype_raw(p);
1969    }
1970        result.set(ctx.common_atoms.length, JSValue::new_int(0));
1971        let ptr = Box::into_raw(Box::new(result)) as usize;
1972        return JSValue::new_object(ptr);
1973    }
1974    let s = match require_string_coercible(ctx, &args[0]) {
1975        Some(s) => s,
1976        None => return JSValue::undefined(),
1977    };
1978
1979    let pattern = if args.len() > 1 {
1980        if args[1].is_object() {
1981            let regexp_obj = args[1].as_object();
1982
1983            let flags_atom = ctx.common_atoms.__flags__;
1984            let flags = if let Some(f) = regexp_obj.get(flags_atom) {
1985                if f.is_string() {
1986                    ctx.get_atom_str(f.get_atom()).to_string()
1987                } else {
1988                    String::new()
1989                }
1990            } else {
1991                String::new()
1992            };
1993
1994            if !flags.contains('g') {
1995                let mut err_obj = JSObject::new();
1996                err_obj.set(
1997                    ctx.common_atoms.name,
1998                    JSValue::new_string(ctx.intern("TypeError")),
1999                );
2000                err_obj.set(
2001                    ctx.common_atoms.message,
2002                    JSValue::new_string(ctx.intern("matchAll requires a global RegExp")),
2003                );
2004                let ptr = Box::into_raw(Box::new(err_obj)) as usize;
2005                return JSValue::new_object(ptr);
2006            }
2007
2008            let pattern_atom = ctx.common_atoms.__pattern__;
2009            if let Some(p) = regexp_obj.get(pattern_atom) {
2010                if p.is_string() {
2011                    ctx.get_atom_str(p.get_atom()).to_string()
2012                } else {
2013                    String::new()
2014                }
2015            } else {
2016                String::new()
2017            }
2018        } else if args[1].is_string() {
2019            ctx.get_atom_str(args[1].get_atom()).to_string()
2020        } else {
2021            String::new()
2022        }
2023    } else {
2024        String::new()
2025    };
2026
2027    let mut result_array = JSObject::new_array();
2028    if let Some(p) = ctx.get_array_prototype() {
2029        result_array.set_prototype_raw(p);
2030    }
2031    let length_atom = ctx.common_atoms.length;
2032    let mut match_count = 0;
2033
2034    if pattern.is_empty() {
2035        result_array.set(length_atom, JSValue::new_int(0));
2036        let ptr = Box::into_raw(Box::new(result_array)) as usize;
2037        return JSValue::new_object(ptr);
2038    }
2039
2040    let mut search_pos = 0;
2041    while search_pos <= s.len() {
2042        let max_start = if pattern.len() <= s.len() {
2043            s.len() - pattern.len()
2044        } else {
2045            break;
2046        };
2047
2048        let mut found = false;
2049        for i in search_pos..=max_start {
2050            if i + pattern.len() <= s.len() {
2051                let slice = &s[i..i + pattern.len()];
2052                if slice == pattern {
2053                    found = true;
2054                    let mut match_obj = JSObject::new();
2055                    match_obj.set(ctx.intern("0"), JSValue::new_string(ctx.intern(slice)));
2056                    match_obj.set(ctx.common_atoms.index, JSValue::new_int(i as i64));
2057                    match_obj.set(ctx.common_atoms.input, JSValue::new_string(ctx.intern(&s)));
2058                    match_obj.set(ctx.common_atoms.length, JSValue::new_int(1));
2059
2060                    let key = ctx.int_atom_mut(match_count);
2061                    result_array.set(
2062                        key,
2063                        JSValue::new_object(Box::into_raw(Box::new(match_obj)) as usize),
2064                    );
2065
2066                    match_count += 1;
2067                    search_pos = i + pattern.len();
2068                    if pattern.len() == 0 {
2069                        search_pos += 1;
2070                    }
2071                    break;
2072                }
2073            }
2074        }
2075
2076        if !found {
2077            break;
2078        }
2079    }
2080
2081    result_array.set(length_atom, JSValue::new_int(match_count as i64));
2082    let ptr = Box::into_raw(Box::new(result_array)) as usize;
2083    JSValue::new_object(ptr)
2084}
2085
2086fn string_search(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
2087    if args.len() < 2 {
2088        return JSValue::new_int(-1);
2089    }
2090    let s = match require_string_coercible(ctx, &args[0]) {
2091        Some(s) => s,
2092        None => return JSValue::undefined(),
2093    };
2094
2095    if args[1].is_object() {
2096        let regexp_obj = args[1].as_object();
2097        let pattern_atom = ctx.common_atoms.__pattern__;
2098        let flags_atom = ctx.common_atoms.__flags__;
2099
2100        let pattern = if let Some(p) = regexp_obj.get(pattern_atom) {
2101            if p.is_string() {
2102                ctx.get_atom_str(p.get_atom()).to_string()
2103            } else {
2104                return JSValue::new_int(-1);
2105            }
2106        } else {
2107            return JSValue::new_int(-1);
2108        };
2109
2110        let flags = if let Some(f) = regexp_obj.get(flags_atom) {
2111            if f.is_string() {
2112                ctx.get_atom_str(f.get_atom()).to_string()
2113            } else {
2114                String::new()
2115            }
2116        } else {
2117            String::new()
2118        };
2119
2120        let ignore_case = flags.contains('i');
2121        let s_lower = if ignore_case {
2122            s.to_lowercase()
2123        } else {
2124            String::new()
2125        };
2126        let p_lower = if ignore_case {
2127            pattern.to_lowercase()
2128        } else {
2129            String::new()
2130        };
2131
2132        let found = if ignore_case {
2133            s_lower.find(&p_lower)
2134        } else {
2135            s.find(&pattern)
2136        };
2137        JSValue::new_int(found.map(|i| i as i64).unwrap_or(-1))
2138    } else if args[1].is_string() {
2139        let search = ctx.get_atom_str(args[1].get_atom()).to_string();
2140        JSValue::new_int(s.find(&search).map(|i| i as i64).unwrap_or(-1))
2141    } else {
2142        JSValue::new_int(-1)
2143    }
2144}
2145
2146fn string_match(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
2147    if args.len() < 2 {
2148        return JSValue::null();
2149    }
2150    let s = match require_string_coercible(ctx, &args[0]) {
2151        Some(s) => s,
2152        None => return JSValue::undefined(),
2153    };
2154
2155    if args[1].is_object() {
2156        let regexp_obj = args[1].as_object();
2157        let pattern_atom = ctx.common_atoms.__pattern__;
2158        let flags_atom = ctx.common_atoms.__flags__;
2159
2160        let pattern = if let Some(p) = regexp_obj.get(pattern_atom) {
2161            if p.is_string() {
2162                ctx.get_atom_str(p.get_atom()).to_string()
2163            } else {
2164                return JSValue::null();
2165            }
2166        } else {
2167            return JSValue::null();
2168        };
2169
2170        let flags = if let Some(f) = regexp_obj.get(flags_atom) {
2171            if f.is_string() {
2172                ctx.get_atom_str(f.get_atom()).to_string()
2173            } else {
2174                String::new()
2175            }
2176        } else {
2177            String::new()
2178        };
2179
2180        let is_global = flags.contains('g');
2181        let ignore_case = flags.contains('i');
2182        let s_lower = if ignore_case {
2183            s.to_lowercase()
2184        } else {
2185            String::new()
2186        };
2187        let p_lower = if ignore_case {
2188            pattern.to_lowercase()
2189        } else {
2190            String::new()
2191        };
2192
2193        if is_global {
2194            let mut results: Vec<String> = Vec::new();
2195            let mut search_from = 0;
2196            loop {
2197                let found = if ignore_case {
2198                    s_lower[search_from..]
2199                        .find(&p_lower)
2200                        .map(|i| i + search_from)
2201                } else {
2202                    s[search_from..].find(&pattern).map(|i| i + search_from)
2203                };
2204                if let Some(pos) = found {
2205                    let end = (pos + pattern.len()).min(s.len());
2206                    results.push(s[pos..end].to_string());
2207                    search_from = end;
2208                    if pattern.is_empty() {
2209                        if search_from < s.len() {
2210                            search_from += 1;
2211                        } else {
2212                            break;
2213                        }
2214                    }
2215                } else {
2216                    break;
2217                }
2218            }
2219            if results.is_empty() {
2220                return JSValue::null();
2221            }
2222            let mut arr = JSObject::new_array();
2223            if let Some(p) = ctx.get_array_prototype() {
2224                arr.set_prototype_raw(p);
2225            }
2226            let length_atom = ctx.common_atoms.length;
2227            for (i, m) in results.iter().enumerate() {
2228                let key = ctx.int_atom_mut(i);
2229                arr.set(key, JSValue::new_string(ctx.intern(m)));
2230            }
2231            arr.set(length_atom, JSValue::new_int(results.len() as i64));
2232            let ptr = Box::into_raw(Box::new(arr)) as usize;
2233            JSValue::new_object(ptr)
2234        } else {
2235            let found = if ignore_case {
2236                s_lower.find(&p_lower)
2237            } else {
2238                s.find(&pattern)
2239            };
2240            if let Some(pos) = found {
2241                let end = (pos + pattern.len()).min(s.len());
2242                let match_str = &s[pos..end];
2243                let mut result = JSObject::new_array();
2244    if let Some(p) = ctx.get_array_prototype() {
2245        result.set_prototype_raw(p);
2246    }
2247                result.set(ctx.intern("0"), JSValue::new_string(ctx.intern(match_str)));
2248                result.set(ctx.common_atoms.index, JSValue::new_int(pos as i64));
2249                result.set(ctx.common_atoms.input, JSValue::new_string(ctx.intern(&s)));
2250                result.set(ctx.common_atoms.length, JSValue::new_int(1));
2251                let ptr = Box::into_raw(Box::new(result)) as usize;
2252                JSValue::new_object(ptr)
2253            } else {
2254                JSValue::null()
2255            }
2256        }
2257    } else {
2258        JSValue::null()
2259    }
2260}
2261
2262fn string_substr(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
2263    let this_str = match this_to_string(ctx, &args[0]) {
2264        Some(s) => s,
2265        None => return JSValue::new_string(ctx.intern("")),
2266    };
2267    let s = &this_str;
2268    let len = s.chars().count();
2269    let start = if args.len() > 1 {
2270        let v = args[1].get_int();
2271        if v < 0 {
2272            ((len as i64) + v).max(0) as usize
2273        } else {
2274            (v as usize).min(len)
2275        }
2276    } else {
2277        0
2278    };
2279
2280    let end = if args.len() > 2 {
2281        let v = args[2].get_int();
2282        if v <= 0 {
2283            return JSValue::new_string(ctx.intern(""));
2284        }
2285        (start + v as usize).min(len)
2286    } else {
2287        len
2288    };
2289    let start_b = byte_index_for_char_pos(&s, start);
2290    let end_b = byte_index_for_char_pos(&s, end);
2291    JSValue::new_string(ctx.intern(&s[start_b..end_b]))
2292}
2293
2294fn string_raw(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
2295    if args.is_empty() {
2296        let mut err =
2297            crate::object::object::JSObject::new_typed(crate::object::object::ObjectType::Error);
2298        err.set(
2299            ctx.common_atoms.name,
2300            JSValue::new_string(ctx.intern("TypeError")),
2301        );
2302        err.set(
2303            ctx.common_atoms.message,
2304            JSValue::new_string(ctx.intern("String.raw requires a template")),
2305        );
2306        if let Some(proto) = ctx.get_type_error_prototype() {
2307            err.prototype = Some(proto);
2308        }
2309        let ptr = Box::into_raw(Box::new(err)) as usize;
2310        ctx.runtime_mut().gc_heap_mut().track(ptr);
2311        ctx.pending_exception = Some(JSValue::new_object(ptr));
2312        return JSValue::undefined();
2313    }
2314    let template = &args[0];
2315    if !template.is_object() {
2316        let mut err =
2317            crate::object::object::JSObject::new_typed(crate::object::object::ObjectType::Error);
2318        err.set(
2319            ctx.common_atoms.name,
2320            JSValue::new_string(ctx.intern("TypeError")),
2321        );
2322        err.set(
2323            ctx.common_atoms.message,
2324            JSValue::new_string(ctx.intern("String.raw requires template to be an object")),
2325        );
2326        if let Some(proto) = ctx.get_type_error_prototype() {
2327            err.prototype = Some(proto);
2328        }
2329        let ptr = Box::into_raw(Box::new(err)) as usize;
2330        ctx.runtime_mut().gc_heap_mut().track(ptr);
2331        ctx.pending_exception = Some(JSValue::new_object(ptr));
2332        return JSValue::undefined();
2333    }
2334    let obj = template.as_object();
2335    let raw_atom = ctx.intern("raw");
2336    let raw_val = match obj.get(raw_atom) {
2337        Some(v) => v,
2338        None => {
2339            let mut err = crate::object::object::JSObject::new_typed(
2340                crate::object::object::ObjectType::Error,
2341            );
2342            err.set(
2343                ctx.common_atoms.name,
2344                JSValue::new_string(ctx.intern("TypeError")),
2345            );
2346            err.set(
2347                ctx.common_atoms.message,
2348                JSValue::new_string(ctx.intern("String.raw template has no raw property")),
2349            );
2350            if let Some(proto) = ctx.get_type_error_prototype() {
2351                err.prototype = Some(proto);
2352            }
2353            let ptr = Box::into_raw(Box::new(err)) as usize;
2354            ctx.runtime_mut().gc_heap_mut().track(ptr);
2355            ctx.pending_exception = Some(JSValue::new_object(ptr));
2356            return JSValue::undefined();
2357        }
2358    };
2359    if !raw_val.is_object() {
2360        let mut err =
2361            crate::object::object::JSObject::new_typed(crate::object::object::ObjectType::Error);
2362        err.set(
2363            ctx.common_atoms.name,
2364            JSValue::new_string(ctx.intern("TypeError")),
2365        );
2366        err.set(
2367            ctx.common_atoms.message,
2368            JSValue::new_string(ctx.intern("String.raw template.raw is not an object")),
2369        );
2370        if let Some(proto) = ctx.get_type_error_prototype() {
2371            err.prototype = Some(proto);
2372        }
2373        let ptr = Box::into_raw(Box::new(err)) as usize;
2374        ctx.runtime_mut().gc_heap_mut().track(ptr);
2375        ctx.pending_exception = Some(JSValue::new_object(ptr));
2376        return JSValue::undefined();
2377    }
2378    let raw_obj = raw_val.as_object();
2379    let length_atom = ctx.common_atoms.length;
2380    let len_val = raw_obj.get(length_atom).unwrap_or(JSValue::new_int(0));
2381    let len = if len_val.is_int() {
2382        len_val.get_int() as usize
2383    } else if len_val.is_float() {
2384        len_val.get_float() as usize
2385    } else {
2386        0
2387    };
2388    if len == 0 {
2389        return JSValue::new_string(ctx.intern(""));
2390    }
2391    let substitutions = if args.len() > 1 { &args[1..] } else { &[] };
2392    let mut result = String::new();
2393    let mut i = 0usize;
2394    while i < len {
2395        let key = ctx.int_atom_mut(i);
2396        let raw_str_val = if raw_obj.is_dense_array() {
2397            let arr_ptr = raw_obj as *const _ as usize;
2398            let arr = unsafe { &*(arr_ptr as *const crate::object::array_obj::JSArrayObject) };
2399            arr.get(i).unwrap_or(JSValue::new_string(ctx.intern("")))
2400        } else {
2401            raw_obj
2402                .get(key)
2403                .unwrap_or(JSValue::new_string(ctx.intern("")))
2404        };
2405        let raw_str = if raw_str_val.is_string() {
2406            ctx.get_atom_str(raw_str_val.get_atom()).to_string()
2407        } else {
2408            js_to_string_arg(&raw_str_val, ctx)
2409        };
2410        result.push_str(&raw_str);
2411        if i + 1 < len {
2412            if i < substitutions.len() {
2413                let sub = &substitutions[i];
2414                let sub_str = if sub.is_string() {
2415                    ctx.get_atom_str(sub.get_atom()).to_string()
2416                } else {
2417                    js_to_string_arg(sub, ctx)
2418                };
2419                result.push_str(&sub_str);
2420            }
2421        }
2422        i += 1;
2423    }
2424    JSValue::new_string(ctx.intern(&result))
2425}