boa/builtins/string/
mod.rs

1//! This module implements the global `String` object.
2//!
3//! The `String` global object is a constructor for strings or a sequence of characters.
4//!
5//! More information:
6//!  - [ECMAScript reference][spec]
7//!  - [MDN documentation][mdn]
8//!
9//! [spec]: https://tc39.es/ecma262/#sec-string-object
10//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String
11
12pub mod string_iterator;
13#[cfg(test)]
14mod tests;
15
16use crate::builtins::Symbol;
17use crate::context::StandardObjects;
18use crate::object::internal_methods::get_prototype_from_constructor;
19use crate::object::JsObject;
20use crate::{
21    builtins::{string::string_iterator::StringIterator, Array, BuiltIn, RegExp},
22    object::{ConstructorBuilder, ObjectData},
23    property::{Attribute, PropertyDescriptor},
24    symbol::WellKnownSymbols,
25    BoaProfiler, Context, JsResult, JsString, JsValue,
26};
27use std::{
28    char::{decode_utf16, from_u32},
29    cmp::{max, min},
30    string::String as StdString,
31};
32use unicode_normalization::UnicodeNormalization;
33
34use super::JsArgs;
35
36pub(crate) fn code_point_at(string: JsString, position: i32) -> Option<(u32, u8, bool)> {
37    let size = string.encode_utf16().count() as i32;
38    if position < 0 || position >= size {
39        return None;
40    }
41    let mut encoded = string.encode_utf16();
42    let first = encoded.nth(position as usize)?;
43    if !is_leading_surrogate(first) && !is_trailing_surrogate(first) {
44        return Some((first as u32, 1, false));
45    }
46    if is_trailing_surrogate(first) || position + 1 == size {
47        return Some((first as u32, 1, true));
48    }
49    let second = encoded.next()?;
50    if !is_trailing_surrogate(second) {
51        return Some((first as u32, 1, true));
52    }
53    let cp = (first as u32 - 0xD800) * 0x400 + (second as u32 - 0xDC00) + 0x10000;
54    Some((cp, 2, false))
55}
56
57/// Helper function to check if a `char` is trimmable.
58#[inline]
59pub(crate) fn is_trimmable_whitespace(c: char) -> bool {
60    // The rust implementation of `trim` does not regard the same characters whitespace as ecma standard does
61    //
62    // Rust uses \p{White_Space} by default, which also includes:
63    // `\u{0085}' (next line)
64    // And does not include:
65    // '\u{FEFF}' (zero width non-breaking space)
66    // Explicit whitespace: https://tc39.es/ecma262/#sec-white-space
67    matches!(
68        c,
69        '\u{0009}' | '\u{000B}' | '\u{000C}' | '\u{0020}' | '\u{00A0}' | '\u{FEFF}' |
70    // Unicode Space_Separator category
71    '\u{1680}' | '\u{2000}'
72            ..='\u{200A}' | '\u{202F}' | '\u{205F}' | '\u{3000}' |
73    // Line terminators: https://tc39.es/ecma262/#sec-line-terminators
74    '\u{000A}' | '\u{000D}' | '\u{2028}' | '\u{2029}'
75    )
76}
77
78pub(crate) fn is_leading_surrogate(value: u16) -> bool {
79    (0xD800..=0xDBFF).contains(&value)
80}
81
82pub(crate) fn is_trailing_surrogate(value: u16) -> bool {
83    (0xDC00..=0xDFFF).contains(&value)
84}
85
86/// JavaScript `String` implementation.
87#[derive(Debug, Clone, Copy)]
88pub(crate) struct String;
89
90impl BuiltIn for String {
91    const NAME: &'static str = "String";
92
93    fn attribute() -> Attribute {
94        Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE
95    }
96
97    fn init(context: &mut Context) -> (&'static str, JsValue, Attribute) {
98        let _timer = BoaProfiler::global().start_event(Self::NAME, "init");
99
100        let symbol_iterator = WellKnownSymbols::iterator();
101
102        let attribute = Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::PERMANENT;
103        let string_object = ConstructorBuilder::with_standard_object(
104            context,
105            Self::constructor,
106            context.standard_objects().string_object().clone(),
107        )
108        .name(Self::NAME)
109        .length(Self::LENGTH)
110        .property("length", 0, attribute)
111        .method(Self::char_at, "charAt", 1)
112        .method(Self::char_code_at, "charCodeAt", 1)
113        .method(Self::code_point_at, "codePointAt", 1)
114        .method(Self::to_string, "toString", 0)
115        .method(Self::concat, "concat", 1)
116        .method(Self::repeat, "repeat", 1)
117        .method(Self::slice, "slice", 2)
118        .method(Self::starts_with, "startsWith", 1)
119        .method(Self::ends_with, "endsWith", 1)
120        .method(Self::includes, "includes", 1)
121        .method(Self::index_of, "indexOf", 1)
122        .method(Self::last_index_of, "lastIndexOf", 1)
123        .method(Self::r#match, "match", 1)
124        .method(Self::normalize, "normalize", 1)
125        .method(Self::pad_end, "padEnd", 1)
126        .method(Self::pad_start, "padStart", 1)
127        .method(Self::trim, "trim", 0)
128        .method(Self::trim_start, "trimStart", 0)
129        .method(Self::trim_end, "trimEnd", 0)
130        .method(Self::to_lowercase, "toLowerCase", 0)
131        .method(Self::to_uppercase, "toUpperCase", 0)
132        .method(Self::substring, "substring", 2)
133        .method(Self::substr, "substr", 2)
134        .method(Self::split, "split", 2)
135        .method(Self::value_of, "valueOf", 0)
136        .method(Self::match_all, "matchAll", 1)
137        .method(Self::replace, "replace", 2)
138        .method(Self::replace_all, "replaceAll", 2)
139        .method(Self::iterator, (symbol_iterator, "[Symbol.iterator]"), 0)
140        .method(Self::search, "search", 1)
141        .method(Self::at, "at", 1)
142        .build();
143
144        (Self::NAME, string_object.into(), Self::attribute())
145    }
146}
147
148impl String {
149    /// The amount of arguments this function object takes.
150    pub(crate) const LENGTH: usize = 1;
151
152    /// JavaScript strings must be between `0` and less than positive `Infinity` and cannot be a negative number.
153    /// The range of allowed values can be described like this: `[0, +∞)`.
154    ///
155    /// The resulting string can also not be larger than the maximum string size,
156    /// which can differ in JavaScript engines. In Boa it is `2^32 - 1`
157    pub(crate) const MAX_STRING_LENGTH: f64 = u32::MAX as f64;
158
159    /// `String( value )`
160    ///
161    /// <https://tc39.es/ecma262/#sec-string-constructor-string-value>
162    pub(crate) fn constructor(
163        new_target: &JsValue,
164        args: &[JsValue],
165        context: &mut Context,
166    ) -> JsResult<JsValue> {
167        // This value is used by console.log and other routines to match Object type
168        // to its Javascript Identifier (global constructor method name)
169        let string = match args.get(0) {
170            Some(value) if value.is_symbol() && new_target.is_undefined() => {
171                Symbol::to_string(value, &[], context)?
172                    .as_string()
173                    .expect("'Symbol::to_string' returns 'Value::String'")
174                    .clone()
175            }
176            Some(value) => value.to_string(context)?,
177            None => JsString::default(),
178        };
179
180        if new_target.is_undefined() {
181            return Ok(string.into());
182        }
183
184        let prototype =
185            get_prototype_from_constructor(new_target, StandardObjects::string_object, context)?;
186        Ok(Self::string_create(string, prototype, context).into())
187    }
188
189    /// Abstract function `StringCreate( value, prototype )`.
190    ///
191    /// Call this function if you want to create a `String` exotic object.
192    ///
193    /// More information:
194    ///  - [ECMAScript reference][spec]
195    ///
196    /// [spec]: https://tc39.es/ecma262/#sec-stringcreate
197    fn string_create(value: JsString, prototype: JsObject, context: &mut Context) -> JsObject {
198        // 7. Let length be the number of code unit elements in value.
199        let len = value.encode_utf16().count();
200
201        // 1. Let S be ! MakeBasicObject(« [[Prototype]], [[Extensible]], [[StringData]] »).
202        // 2. Set S.[[Prototype]] to prototype.
203        // 3. Set S.[[StringData]] to value.
204        // 4. Set S.[[GetOwnProperty]] as specified in 10.4.3.1.
205        // 5. Set S.[[DefineOwnProperty]] as specified in 10.4.3.2.
206        // 6. Set S.[[OwnPropertyKeys]] as specified in 10.4.3.3.
207        let s = context.construct_object();
208        s.set_prototype_instance(prototype.into());
209        s.borrow_mut().data = ObjectData::string(value);
210
211        // 8. Perform ! DefinePropertyOrThrow(S, "length", PropertyDescriptor { [[Value]]: 𝔽(length),
212        // [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: false }).
213        s.define_property_or_throw(
214            "length",
215            PropertyDescriptor::builder()
216                .value(len)
217                .writable(false)
218                .enumerable(false)
219                .configurable(false),
220            context,
221        )
222        .expect("length definition for a new string must not fail");
223
224        // 9. Return S.
225        s
226    }
227
228    fn this_string_value(this: &JsValue, context: &mut Context) -> JsResult<JsString> {
229        match this {
230            JsValue::String(ref string) => return Ok(string.clone()),
231            JsValue::Object(ref object) => {
232                let object = object.borrow();
233                if let Some(string) = object.as_string() {
234                    return Ok(string);
235                }
236            }
237            _ => {}
238        }
239
240        Err(context.construct_type_error("'this' is not a string"))
241    }
242
243    /// Get the string value to a primitive string
244    #[allow(clippy::wrong_self_convention)]
245    #[inline]
246    pub(crate) fn to_string(
247        this: &JsValue,
248        _: &[JsValue],
249        context: &mut Context,
250    ) -> JsResult<JsValue> {
251        // Get String from String Object and send it back as a new value
252        Ok(JsValue::new(Self::this_string_value(this, context)?))
253    }
254
255    /// `String.prototype.charAt( index )`
256    ///
257    /// The `String` object's `charAt()` method returns a new string consisting of the single UTF-16 code unit located at the specified offset into the string.
258    ///
259    /// Characters in a string are indexed from left to right. The index of the first character is `0`,
260    /// and the index of the last character—in a string called `stringName`—is `stringName.length - 1`.
261    /// If the `index` you supply is out of this range, JavaScript returns an empty string.
262    ///
263    /// If no index is provided to `charAt()`, the default is `0`.
264    ///
265    /// More information:
266    ///  - [ECMAScript reference][spec]
267    ///  - [MDN documentation][mdn]
268    ///
269    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.charat
270    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charAt
271    pub(crate) fn char_at(
272        this: &JsValue,
273        args: &[JsValue],
274        context: &mut Context,
275    ) -> JsResult<JsValue> {
276        // First we get it the actual string a private field stored on the object only the context has access to.
277        // Then we convert it into a Rust String by wrapping it in from_value
278        let primitive_val = this.to_string(context)?;
279        let pos = args
280            .get(0)
281            .cloned()
282            .unwrap_or_else(JsValue::undefined)
283            .to_integer(context)? as i32;
284
285        // Fast path returning empty string when pos is obviously out of range
286        if pos < 0 || pos >= primitive_val.len() as i32 {
287            return Ok("".into());
288        }
289
290        // Calling .len() on a string would give the wrong result, as they are bytes not the number of
291        // unicode code points
292        // Note that this is an O(N) operation (because UTF-8 is complex) while getting the number of
293        // bytes is an O(1) operation.
294        if let Some(utf16_val) = primitive_val.encode_utf16().nth(pos as usize) {
295            Ok(JsValue::new(from_u32(utf16_val as u32).unwrap()))
296        } else {
297            Ok("".into())
298        }
299    }
300
301    /// `String.prototype.at ( index )`
302    ///
303    /// This String object's at() method returns a String consisting of the single UTF-16 code unit located at the specified position.
304    /// Returns undefined if the given index cannot be found.
305    ///
306    /// More information:
307    ///  - [ECMAScript reference][spec]
308    ///  - [MDN documentation][mdn]
309    ///
310    /// [spec]: https://tc39.es/proposal-relative-indexing-method/#sec-string.prototype.at
311    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/at
312    pub(crate) fn at(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
313        let this = this.require_object_coercible(context)?;
314        let s = this.to_string(context)?;
315        let len = s.encode_utf16().count();
316        let relative_index = args
317            .get(0)
318            .cloned()
319            .unwrap_or_default()
320            .to_integer(context)?;
321        let k = if relative_index < 0 as f64 {
322            len - (-relative_index as usize)
323        } else {
324            relative_index as usize
325        };
326
327        if let Some(utf16_val) = s.encode_utf16().nth(k) {
328            Ok(JsValue::new(
329                from_u32(u32::from(utf16_val)).expect("invalid utf-16 character"),
330            ))
331        } else {
332            Ok(JsValue::undefined())
333        }
334    }
335
336    /// `String.prototype.codePointAt( index )`
337    ///
338    /// The `codePointAt()` method returns an integer between `0` to `1114111` (`0x10FFFF`) representing the UTF-16 code unit at the given index.
339    ///
340    /// If no UTF-16 surrogate pair begins at the index, the code point at the index is returned.
341    ///
342    /// `codePointAt()` returns `undefined` if the given index is less than `0`, or if it is equal to or greater than the `length` of the string.
343    ///
344    /// More information:
345    ///  - [ECMAScript reference][spec]
346    ///  - [MDN documentation][mdn]
347    ///
348    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.codepointat
349    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/codePointAt
350    pub(crate) fn code_point_at(
351        this: &JsValue,
352        args: &[JsValue],
353        context: &mut Context,
354    ) -> JsResult<JsValue> {
355        // First we get it the actual string a private field stored on the object only the context has access to.
356        // Then we convert it into a Rust String by wrapping it in from_value
357        let primitive_val = this.to_string(context)?;
358        let pos = args
359            .get(0)
360            .cloned()
361            .unwrap_or_else(JsValue::undefined)
362            .to_integer(context)? as i32;
363
364        // Fast path returning undefined when pos is obviously out of range
365        if pos < 0 || pos >= primitive_val.len() as i32 {
366            return Ok(JsValue::undefined());
367        }
368
369        if let Some((code_point, _, _)) = code_point_at(primitive_val, pos) {
370            Ok(JsValue::new(code_point))
371        } else {
372            Ok(JsValue::undefined())
373        }
374    }
375
376    /// `String.prototype.charCodeAt( index )`
377    ///
378    /// The `charCodeAt()` method returns an integer between `0` and `65535` representing the UTF-16 code unit at the given index.
379    ///
380    /// Unicode code points range from `0` to `1114111` (`0x10FFFF`). The first 128 Unicode code points are a direct match of the ASCII character encoding.
381    ///
382    /// `charCodeAt()` returns `NaN` if the given index is less than `0`, or if it is equal to or greater than the `length` of the string.
383    ///
384    /// More information:
385    ///  - [ECMAScript reference][spec]
386    ///  - [MDN documentation][mdn]
387    ///
388    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.charcodeat
389    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt
390    pub(crate) fn char_code_at(
391        this: &JsValue,
392        args: &[JsValue],
393        context: &mut Context,
394    ) -> JsResult<JsValue> {
395        // First we get it the actual string a private field stored on the object only the context has access to.
396        // Then we convert it into a Rust String by wrapping it in from_value
397        let primitive_val = this.to_string(context)?;
398        let pos = args
399            .get(0)
400            .cloned()
401            .unwrap_or_else(JsValue::undefined)
402            .to_integer(context)? as i32;
403
404        // Fast path returning NaN when pos is obviously out of range
405        if pos < 0 || pos >= primitive_val.len() as i32 {
406            return Ok(JsValue::nan());
407        }
408
409        // Calling .len() on a string would give the wrong result, as they are bytes not the number of unicode code points
410        // Note that this is an O(N) operation (because UTF-8 is complex) while getting the number of bytes is an O(1) operation.
411        // If there is no element at that index, the result is NaN
412        if let Some(utf16_val) = primitive_val.encode_utf16().nth(pos as usize) {
413            Ok(JsValue::new(f64::from(utf16_val)))
414        } else {
415            Ok(JsValue::nan())
416        }
417    }
418
419    /// `String.prototype.concat( str1[, ...strN] )`
420    ///
421    /// The `concat()` method concatenates the string arguments to the calling string and returns a new string.
422    ///
423    /// Changes to the original string or the returned string don't affect the other.
424    ///
425    /// If the arguments are not of the type string, they are converted to string values before concatenating.
426    ///
427    /// More information:
428    ///  - [ECMAScript reference][spec]
429    ///  - [MDN documentation][mdn]
430    ///
431    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.concat
432    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/concat
433    pub(crate) fn concat(
434        this: &JsValue,
435        args: &[JsValue],
436        context: &mut Context,
437    ) -> JsResult<JsValue> {
438        let object = this.require_object_coercible(context)?;
439        let mut string = object.to_string(context)?.to_string();
440
441        for arg in args {
442            string.push_str(&arg.to_string(context)?);
443        }
444
445        Ok(JsValue::new(string))
446    }
447
448    /// `String.prototype.repeat( count )`
449    ///
450    /// The `repeat()` method constructs and returns a new string which contains the specified number of
451    /// copies of the string on which it was called, concatenated together.
452    ///
453    /// More information:
454    ///  - [ECMAScript reference][spec]
455    ///  - [MDN documentation][mdn]
456    ///
457    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.repeat
458    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat
459    pub(crate) fn repeat(
460        this: &JsValue,
461        args: &[JsValue],
462        context: &mut Context,
463    ) -> JsResult<JsValue> {
464        let object = this.require_object_coercible(context)?;
465        let string = object.to_string(context)?;
466
467        if let Some(arg) = args.get(0) {
468            let n = arg.to_integer(context)?;
469            if n < 0.0 {
470                return context.throw_range_error("repeat count cannot be a negative number");
471            }
472
473            if n.is_infinite() {
474                return context.throw_range_error("repeat count cannot be infinity");
475            }
476
477            if n * (string.len() as f64) > Self::MAX_STRING_LENGTH {
478                return context
479                    .throw_range_error("repeat count must not overflow maximum string length");
480            }
481            Ok(string.repeat(n as usize).into())
482        } else {
483            Ok("".into())
484        }
485    }
486
487    /// `String.prototype.slice( beginIndex [, endIndex] )`
488    ///
489    /// The `slice()` method extracts a section of a string and returns it as a new string, without modifying the original string.
490    ///
491    /// More information:
492    ///  - [ECMAScript reference][spec]
493    ///  - [MDN documentation][mdn]
494    ///
495    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.slice
496    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/slice
497    pub(crate) fn slice(
498        this: &JsValue,
499        args: &[JsValue],
500        context: &mut Context,
501    ) -> JsResult<JsValue> {
502        // First we get it the actual string a private field stored on the object only the context has access to.
503        // Then we convert it into a Rust String by wrapping it in from_value
504        let primitive_val = this.to_string(context)?;
505
506        // Calling .len() on a string would give the wrong result, as they are bytes not the number of unicode code points
507        // Note that this is an O(N) operation (because UTF-8 is complex) while getting the number of bytes is an O(1) operation.
508        let length = primitive_val.chars().count() as i32;
509
510        let start = args
511            .get(0)
512            .cloned()
513            .unwrap_or_else(JsValue::undefined)
514            .to_integer(context)? as i32;
515        let end = args
516            .get(1)
517            .cloned()
518            .unwrap_or_else(|| JsValue::new(length))
519            .to_integer(context)? as i32;
520
521        let from = if start < 0 {
522            max(length.wrapping_add(start), 0)
523        } else {
524            min(start, length)
525        };
526        let to = if end < 0 {
527            max(length.wrapping_add(end), 0)
528        } else {
529            min(end, length)
530        };
531
532        let span = max(to.wrapping_sub(from), 0);
533
534        let new_str: StdString = primitive_val
535            .chars()
536            .skip(from as usize)
537            .take(span as usize)
538            .collect();
539        Ok(JsValue::new(new_str))
540    }
541
542    /// `String.prototype.startWith( searchString[, position] )`
543    ///
544    /// The `startsWith()` method determines whether a string begins with the characters of a specified string, returning `true` or `false` as appropriate.
545    ///
546    /// More information:
547    ///  - [ECMAScript reference][spec]
548    ///  - [MDN documentation][mdn]
549    ///
550    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.startswith
551    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith
552    pub(crate) fn starts_with(
553        this: &JsValue,
554        args: &[JsValue],
555        context: &mut Context,
556    ) -> JsResult<JsValue> {
557        // First we get it the actual string a private field stored on the object only the context has access to.
558        // Then we convert it into a Rust String by wrapping it in from_value
559        let primitive_val = this.to_string(context)?;
560
561        let arg = args.get_or_undefined(0);
562
563        if Self::is_regexp_object(arg) {
564            context.throw_type_error(
565                "First argument to String.prototype.startsWith must not be a regular expression",
566            )?;
567        }
568
569        let search_string = arg.to_string(context)?;
570
571        let length = primitive_val.chars().count() as i32;
572        let search_length = search_string.chars().count() as i32;
573
574        // If less than 2 args specified, position is 'undefined', defaults to 0
575        let position = if let Some(integer) = args.get(1) {
576            integer.to_integer(context)? as i32
577        } else {
578            0
579        };
580
581        let start = min(max(position, 0), length);
582        let end = start.wrapping_add(search_length);
583
584        if end > length {
585            Ok(JsValue::new(false))
586        } else {
587            // Only use the part of the string from "start"
588            let this_string: StdString = primitive_val.chars().skip(start as usize).collect();
589            Ok(JsValue::new(
590                this_string.starts_with(search_string.as_str()),
591            ))
592        }
593    }
594
595    /// `String.prototype.endsWith( searchString[, length] )`
596    ///
597    /// The `endsWith()` method determines whether a string ends with the characters of a specified string, returning `true` or `false` as appropriate.
598    ///
599    /// More information:
600    ///  - [ECMAScript reference][spec]
601    ///  - [MDN documentation][mdn]
602    ///
603    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.endswith
604    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith
605    pub(crate) fn ends_with(
606        this: &JsValue,
607        args: &[JsValue],
608        context: &mut Context,
609    ) -> JsResult<JsValue> {
610        // First we get it the actual string a private field stored on the object only the context has access to.
611        // Then we convert it into a Rust String by wrapping it in from_value
612        let primitive_val = this.to_string(context)?;
613
614        let arg = args.get_or_undefined(0);
615
616        if Self::is_regexp_object(arg) {
617            context.throw_type_error(
618                "First argument to String.prototype.endsWith must not be a regular expression",
619            )?;
620        }
621
622        let search_string = arg.to_string(context)?;
623
624        let length = primitive_val.chars().count() as i32;
625        let search_length = search_string.chars().count() as i32;
626
627        // If less than 2 args specified, end_position is 'undefined', defaults to
628        // length of this
629        let end_position = if let Some(integer) = args.get(1) {
630            integer.to_integer(context)? as i32
631        } else {
632            length
633        };
634
635        let end = min(max(end_position, 0), length);
636        let start = end.wrapping_sub(search_length);
637
638        if start < 0 {
639            Ok(JsValue::new(false))
640        } else {
641            // Only use the part of the string up to "end"
642            let this_string: StdString = primitive_val.chars().take(end as usize).collect();
643            Ok(JsValue::new(this_string.ends_with(search_string.as_str())))
644        }
645    }
646
647    /// `String.prototype.includes( searchString[, position] )`
648    ///
649    /// The `includes()` method determines whether one string may be found within another string, returning `true` or `false` as appropriate.
650    ///
651    /// More information:
652    ///  - [ECMAScript reference][spec]
653    ///  - [MDN documentation][mdn]
654    ///
655    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.includes
656    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes
657    pub(crate) fn includes(
658        this: &JsValue,
659        args: &[JsValue],
660        context: &mut Context,
661    ) -> JsResult<JsValue> {
662        // First we get it the actual string a private field stored on the object only the context has access to.
663        // Then we convert it into a Rust String by wrapping it in from_value
664        let primitive_val = this.to_string(context)?;
665
666        let arg = args.get_or_undefined(0);
667
668        if Self::is_regexp_object(arg) {
669            context.throw_type_error(
670                "First argument to String.prototype.includes must not be a regular expression",
671            )?;
672        }
673
674        let search_string = arg.to_string(context)?;
675
676        let length = primitive_val.chars().count() as i32;
677
678        // If less than 2 args specified, position is 'undefined', defaults to 0
679
680        let position = if let Some(integer) = args.get(1) {
681            integer.to_integer(context)? as i32
682        } else {
683            0
684        };
685
686        let start = min(max(position, 0), length);
687
688        // Take the string from "this" and use only the part of it after "start"
689        let this_string: StdString = primitive_val.chars().skip(start as usize).collect();
690
691        Ok(JsValue::new(this_string.contains(search_string.as_str())))
692    }
693
694    fn is_regexp_object(value: &JsValue) -> bool {
695        match value {
696            JsValue::Object(ref obj) => obj.borrow().is_regexp(),
697            _ => false,
698        }
699    }
700
701    /// `String.prototype.replace( regexp|substr, newSubstr|function )`
702    ///
703    /// The `replace()` method returns a new string with some or all matches of a `pattern` replaced by a `replacement`.
704    ///
705    /// The `pattern` can be a string or a `RegExp`, and the `replacement` can be a string or a function to be called for each match.
706    /// If `pattern` is a string, only the first occurrence will be replaced.
707    ///
708    /// The original string is left unchanged.
709    ///
710    /// More information:
711    ///  - [ECMAScript reference][spec]
712    ///  - [MDN documentation][mdn]
713    ///
714    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.replace
715    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace
716    pub(crate) fn replace(
717        this: &JsValue,
718        args: &[JsValue],
719        context: &mut Context,
720    ) -> JsResult<JsValue> {
721        // 1. Let O be ? RequireObjectCoercible(this value).
722        this.require_object_coercible(context)?;
723
724        let search_value = args.get_or_undefined(0);
725
726        let replace_value = args.get_or_undefined(1);
727
728        // 2. If searchValue is neither undefined nor null, then
729        if !search_value.is_null_or_undefined() {
730            // a. Let replacer be ? GetMethod(searchValue, @@replace).
731            let replacer = search_value
732                .as_object()
733                .unwrap_or_default()
734                .get_method(context, WellKnownSymbols::replace())?;
735
736            // b. If replacer is not undefined, then
737            if let Some(replacer) = replacer {
738                // i. Return ? Call(replacer, searchValue, « O, replaceValue »).
739                return context.call(
740                    &replacer.into(),
741                    search_value,
742                    &[this.clone(), replace_value.clone()],
743                );
744            }
745        }
746
747        // 3. Let string be ? ToString(O).
748        let this_str = this.to_string(context)?;
749
750        // 4. Let searchString be ? ToString(searchValue).
751        let search_str = search_value.to_string(context)?;
752
753        // 5. Let functionalReplace be IsCallable(replaceValue).
754        let functional_replace = replace_value.is_function();
755
756        // 6. If functionalReplace is false, then
757        // a. Set replaceValue to ? ToString(replaceValue).
758
759        // 7. Let searchLength be the length of searchString.
760        let search_length = search_str.len();
761
762        // 8. Let position be ! StringIndexOf(string, searchString, 0).
763        // 9. If position is -1, return string.
764        let position = if let Some(p) = this_str.index_of(&search_str, 0) {
765            p
766        } else {
767            return Ok(this_str.into());
768        };
769
770        // 10. Let preserved be the substring of string from 0 to position.
771        let preserved = StdString::from_utf16_lossy(
772            &this_str.encode_utf16().take(position).collect::<Vec<u16>>(),
773        );
774
775        // 11. If functionalReplace is true, then
776        // 12. Else,
777        let replacement = if functional_replace {
778            // a. Let replacement be ? ToString(? Call(replaceValue, undefined, « searchString, 𝔽(position), string »)).
779            context
780                .call(
781                    replace_value,
782                    &JsValue::undefined(),
783                    &[search_str.into(), position.into(), this_str.clone().into()],
784                )?
785                .to_string(context)?
786        } else {
787            // a. Assert: Type(replaceValue) is String.
788            // b. Let captures be a new empty List.
789            let captures = Vec::new();
790
791            // c. Let replacement be ! GetSubstitution(searchString, string, position, captures, undefined, replaceValue).
792            get_substitution(
793                search_str.to_string(),
794                this_str.to_string(),
795                position,
796                captures,
797                JsValue::undefined(),
798                replace_value.to_string(context)?,
799                context,
800            )?
801        };
802
803        // 13. Return the string-concatenation of preserved, replacement, and the substring of string from position + searchLength.
804        Ok(format!(
805            "{}{}{}",
806            preserved,
807            replacement,
808            StdString::from_utf16_lossy(
809                &this_str
810                    .encode_utf16()
811                    .skip(position + search_length)
812                    .collect::<Vec<u16>>()
813            )
814        )
815        .into())
816    }
817
818    /// `22.1.3.18 String.prototype.replaceAll ( searchValue, replaceValue )`
819    ///
820    /// The replaceAll() method returns a new string with all matches of a pattern replaced by a replacement.
821    ///
822    /// The pattern can be a string or a RegExp, and the replacement can be a string or a function to be called for each match.
823    ///
824    /// The original string is left unchanged.
825    ///
826    /// More information:
827    ///  - [ECMAScript reference][spec]
828    ///  - [MDN documentation][mdn]
829    ///
830    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.replaceall
831    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace
832    pub(crate) fn replace_all(
833        this: &JsValue,
834        args: &[JsValue],
835        context: &mut Context,
836    ) -> JsResult<JsValue> {
837        // 1. Let O be ? RequireObjectCoercible(this value).
838        let o = this.require_object_coercible(context)?;
839
840        let search_value = args.get_or_undefined(0);
841        let replace_value = args.get_or_undefined(1);
842
843        // 2. If searchValue is neither undefined nor null, then
844        if !search_value.is_null_or_undefined() {
845            // a. Let isRegExp be ? IsRegExp(searchValue).
846            if let Some(obj) = search_value.as_object() {
847                // b. If isRegExp is true, then
848                if obj.is_regexp() {
849                    // i. Let flags be ? Get(searchValue, "flags").
850                    let flags = obj.get("flags", context)?;
851
852                    // ii. Perform ? RequireObjectCoercible(flags).
853                    flags.require_object_coercible(context)?;
854
855                    // iii. If ? ToString(flags) does not contain "g", throw a TypeError exception.
856                    if !flags.to_string(context)?.contains('g') {
857                        return context.throw_type_error(
858                            "String.prototype.replaceAll called with a non-global RegExp argument",
859                        );
860                    }
861                }
862            }
863
864            // c. Let replacer be ? GetMethod(searchValue, @@replace).
865            let replacer = search_value
866                .as_object()
867                .unwrap_or_default()
868                .get_method(context, WellKnownSymbols::replace())?;
869
870            // d. If replacer is not undefined, then
871            if let Some(replacer) = replacer {
872                // i. Return ? Call(replacer, searchValue, « O, replaceValue »).
873                return replacer.call(search_value, &[o.into(), replace_value.clone()], context);
874            }
875        }
876
877        // 3. Let string be ? ToString(O).
878        let string = o.to_string(context)?;
879
880        // 4. Let searchString be ? ToString(searchValue).
881        let search_string = search_value.to_string(context)?;
882
883        // 5. Let functionalReplace be IsCallable(replaceValue).
884        let functional_replace = replace_value.is_function();
885
886        // 6. If functionalReplace is false, then
887        let replace_value_string = if !functional_replace {
888            // a. Set replaceValue to ? ToString(replaceValue).
889            replace_value.to_string(context)?
890        } else {
891            JsString::new("")
892        };
893
894        // 7. Let searchLength be the length of searchString.
895        let search_length = search_string.encode_utf16().count();
896
897        // 8. Let advanceBy be max(1, searchLength).
898        let advance_by = max(1, search_length);
899
900        // 9. Let matchPositions be a new empty List.
901        let mut match_positions = Vec::new();
902
903        // 10. Let position be ! StringIndexOf(string, searchString, 0).
904        let mut position = string.index_of(&search_string, 0);
905
906        // 11. Repeat, while position is not -1,
907        while let Some(p) = position {
908            // a. Append position to the end of matchPositions.
909            match_positions.push(p);
910
911            // b. Set position to ! StringIndexOf(string, searchString, position + advanceBy).
912            position = string.index_of(&search_string, p + advance_by);
913        }
914
915        // 12. Let endOfLastMatch be 0.
916        let mut end_of_last_match = 0;
917
918        // 13. Let result be the empty String.
919        let mut result = JsString::new("");
920
921        // 14. For each element p of matchPositions, do
922        for p in match_positions {
923            // a. Let preserved be the substring of string from endOfLastMatch to p.
924            let preserved = StdString::from_utf16_lossy(
925                &string
926                    .clone()
927                    .encode_utf16()
928                    .skip(end_of_last_match)
929                    .take(p - end_of_last_match)
930                    .collect::<Vec<u16>>(),
931            );
932
933            // b. If functionalReplace is true, then
934            // c. Else,
935            let replacement = if functional_replace {
936                // i. Let replacement be ? ToString(? Call(replaceValue, undefined, « searchString, 𝔽(p), string »)).
937                context
938                    .call(
939                        replace_value,
940                        &JsValue::undefined(),
941                        &[
942                            search_string.clone().into(),
943                            p.into(),
944                            string.clone().into(),
945                        ],
946                    )?
947                    .to_string(context)?
948            } else {
949                // i. Assert: Type(replaceValue) is String.
950                // ii. Let captures be a new empty List.
951                // iii. Let replacement be ! GetSubstitution(searchString, string, p, captures, undefined, replaceValue).
952                get_substitution(
953                    search_string.to_string(),
954                    string.to_string(),
955                    p,
956                    Vec::new(),
957                    JsValue::undefined(),
958                    replace_value_string.clone(),
959                    context,
960                )
961                .expect("GetSubstitution should never fail here.")
962            };
963            // d. Set result to the string-concatenation of result, preserved, and replacement.
964            result = JsString::new(format!("{}{}{}", result.as_str(), &preserved, &replacement));
965
966            // e. Set endOfLastMatch to p + searchLength.
967            end_of_last_match = p + search_length;
968        }
969
970        // 15. If endOfLastMatch < the length of string, then
971        if end_of_last_match < string.encode_utf16().count() {
972            // a. Set result to the string-concatenation of result and the substring of string from endOfLastMatch.
973            result = JsString::new(format!(
974                "{}{}",
975                result.as_str(),
976                &StdString::from_utf16_lossy(
977                    &string
978                        .encode_utf16()
979                        .skip(end_of_last_match)
980                        .collect::<Vec<u16>>()
981                )
982            ));
983        }
984
985        // 16. Return result.
986        Ok(result.into())
987    }
988
989    /// `String.prototype.indexOf( searchValue[, fromIndex] )`
990    ///
991    /// The `indexOf()` method returns the index within the calling `String` object of the first occurrence
992    /// of the specified value, starting the search at `fromIndex`.
993    ///
994    /// Returns `-1` if the value is not found.
995    ///
996    /// More information:
997    ///  - [ECMAScript reference][spec]
998    ///  - [MDN documentation][mdn]
999    ///
1000    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.indexof
1001    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/indexOf
1002    pub(crate) fn index_of(
1003        this: &JsValue,
1004        args: &[JsValue],
1005        context: &mut Context,
1006    ) -> JsResult<JsValue> {
1007        let this = this.require_object_coercible(context)?;
1008        let string = this.to_string(context)?;
1009
1010        let search_string = args
1011            .get(0)
1012            .cloned()
1013            .unwrap_or_else(JsValue::undefined)
1014            .to_string(context)?;
1015
1016        let length = string.chars().count();
1017        let start = args
1018            .get(1)
1019            .map(|position| position.to_integer(context))
1020            .transpose()?
1021            .map_or(0, |position| position.max(0.0).min(length as f64) as usize);
1022
1023        if search_string.is_empty() {
1024            return Ok(start.min(length).into());
1025        }
1026
1027        if start < length {
1028            if let Some(position) = string.find(search_string.as_str()) {
1029                return Ok(string[..position].chars().count().into());
1030            }
1031        }
1032
1033        Ok(JsValue::new(-1))
1034    }
1035
1036    /// `String.prototype.lastIndexOf( searchValue[, fromIndex] )`
1037    ///
1038    /// The `lastIndexOf()` method returns the index within the calling `String` object of the last occurrence
1039    /// of the specified value, searching backwards from `fromIndex`.
1040    ///
1041    /// Returns `-1` if the value is not found.
1042    ///
1043    /// More information:
1044    ///  - [ECMAScript reference][spec]
1045    ///  - [MDN documentation][mdn]
1046    ///
1047    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.lastindexof
1048    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/lastIndexOf
1049    pub(crate) fn last_index_of(
1050        this: &JsValue,
1051        args: &[JsValue],
1052        context: &mut Context,
1053    ) -> JsResult<JsValue> {
1054        let this = this.require_object_coercible(context)?;
1055        let string = this.to_string(context)?;
1056
1057        let search_string = args
1058            .get(0)
1059            .cloned()
1060            .unwrap_or_else(JsValue::undefined)
1061            .to_string(context)?;
1062
1063        let length = string.chars().count();
1064        let start = args
1065            .get(1)
1066            .map(|position| position.to_integer(context))
1067            .transpose()?
1068            .map_or(0, |position| position.max(0.0).min(length as f64) as usize);
1069
1070        if search_string.is_empty() {
1071            return Ok(start.min(length).into());
1072        }
1073
1074        if start < length {
1075            if let Some(position) = string.rfind(search_string.as_str()) {
1076                return Ok(string[..position].chars().count().into());
1077            }
1078        }
1079
1080        Ok(JsValue::new(-1))
1081    }
1082
1083    /// `String.prototype.match( regexp )`
1084    ///
1085    /// The `match()` method retrieves the result of matching a **string** against a [`regular expression`][regex].
1086    ///
1087    /// More information:
1088    ///  - [ECMAScript reference][spec]
1089    ///  - [MDN documentation][mdn]
1090    ///
1091    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.match
1092    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match
1093    /// [regex]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
1094    pub(crate) fn r#match(
1095        this: &JsValue,
1096        args: &[JsValue],
1097        context: &mut Context,
1098    ) -> JsResult<JsValue> {
1099        // 1. Let O be ? RequireObjectCoercible(this value).
1100        let o = this.require_object_coercible(context)?;
1101
1102        // 2. If regexp is neither undefined nor null, then
1103        let regexp = args.get_or_undefined(0);
1104        if !regexp.is_null_or_undefined() {
1105            // a. Let matcher be ? GetMethod(regexp, @@match).
1106            // b. If matcher is not undefined, then
1107            if let Some(obj) = regexp.as_object() {
1108                if let Some(matcher) = obj.get_method(context, WellKnownSymbols::match_())? {
1109                    // i. Return ? Call(matcher, regexp, « O »).
1110                    return matcher.call(regexp, &[o.clone()], context);
1111                }
1112            }
1113        }
1114
1115        // 3. Let S be ? ToString(O).
1116        let s = o.to_string(context)?;
1117
1118        // 4. Let rx be ? RegExpCreate(regexp, undefined).
1119        let rx = RegExp::create(regexp.clone(), JsValue::undefined(), context)?;
1120
1121        // 5. Return ? Invoke(rx, @@match, « S »).
1122        let obj = rx.as_object().expect("RegExpCreate must return Object");
1123        if let Some(matcher) = obj.get_method(context, WellKnownSymbols::match_())? {
1124            matcher.call(&rx, &[JsValue::new(s)], context)
1125        } else {
1126            context.throw_type_error("RegExp[Symbol.match] is undefined")
1127        }
1128    }
1129
1130    /// Abstract method `StringPad`.
1131    ///
1132    /// Performs the actual string padding for padStart/End.
1133    /// <https://tc39.es/ecma262/#sec-stringpad/>
1134    fn string_pad(
1135        primitive: JsString,
1136        max_length: i32,
1137        fill_string: Option<JsString>,
1138        at_start: bool,
1139    ) -> JsValue {
1140        let primitive_length = primitive.len() as i32;
1141
1142        if max_length <= primitive_length {
1143            return JsValue::new(primitive);
1144        }
1145
1146        let filler = fill_string.as_deref().unwrap_or(" ");
1147
1148        if filler.is_empty() {
1149            return JsValue::new(primitive);
1150        }
1151
1152        let fill_len = max_length.wrapping_sub(primitive_length);
1153        let mut fill_str = StdString::new();
1154
1155        while fill_str.len() < fill_len as usize {
1156            fill_str.push_str(filler);
1157        }
1158        // Cut to size max_length
1159        let concat_fill_str: StdString = fill_str.chars().take(fill_len as usize).collect();
1160
1161        if at_start {
1162            JsValue::new(format!("{}{}", concat_fill_str, &primitive))
1163        } else {
1164            JsValue::new(format!("{}{}", primitive, &concat_fill_str))
1165        }
1166    }
1167
1168    /// `String.prototype.padEnd( targetLength[, padString] )`
1169    ///
1170    /// The `padEnd()` method pads the current string with a given string (repeated, if needed) so that the resulting string reaches a given length.
1171    ///
1172    /// The padding is applied from the end of the current string.
1173    ///
1174    /// More information:
1175    ///  - [ECMAScript reference][spec]
1176    ///  - [MDN documentation][mdn]
1177    ///
1178    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.padend
1179    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padEnd
1180    pub(crate) fn pad_end(
1181        this: &JsValue,
1182        args: &[JsValue],
1183        context: &mut Context,
1184    ) -> JsResult<JsValue> {
1185        let primitive = this.to_string(context)?;
1186        if args.is_empty() {
1187            return Err(JsValue::new("padEnd requires maxLength argument"));
1188        }
1189        let max_length = args
1190            .get(0)
1191            .expect("failed to get argument for String method")
1192            .to_integer(context)? as i32;
1193
1194        let fill_string = args.get(1).map(|arg| arg.to_string(context)).transpose()?;
1195
1196        Ok(Self::string_pad(primitive, max_length, fill_string, false))
1197    }
1198
1199    /// `String.prototype.padStart( targetLength [, padString] )`
1200    ///
1201    /// The `padStart()` method pads the current string with another string (multiple times, if needed) until the resulting string reaches the given length.
1202    ///
1203    /// The padding is applied from the start of the current string.
1204    ///
1205    /// More information:
1206    ///  - [ECMAScript reference][spec]
1207    ///  - [MDN documentation][mdn]
1208    ///
1209    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.padstart
1210    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart
1211    pub(crate) fn pad_start(
1212        this: &JsValue,
1213        args: &[JsValue],
1214        context: &mut Context,
1215    ) -> JsResult<JsValue> {
1216        let primitive = this.to_string(context)?;
1217        if args.is_empty() {
1218            return Err(JsValue::new("padStart requires maxLength argument"));
1219        }
1220        let max_length = args
1221            .get(0)
1222            .expect("failed to get argument for String method")
1223            .to_integer(context)? as i32;
1224
1225        let fill_string = args.get(1).map(|arg| arg.to_string(context)).transpose()?;
1226
1227        Ok(Self::string_pad(primitive, max_length, fill_string, true))
1228    }
1229
1230    /// String.prototype.trim()
1231    ///
1232    /// The `trim()` method removes whitespace from both ends of a string.
1233    ///
1234    /// Whitespace in this context is all the whitespace characters (space, tab, no-break space, etc.) and all the line terminator characters (LF, CR, etc.).
1235    ///
1236    /// More information:
1237    ///  - [ECMAScript reference][spec]
1238    ///  - [MDN documentation][mdn]
1239    ///
1240    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.trim
1241    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trim
1242    pub(crate) fn trim(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
1243        let this = this.require_object_coercible(context)?;
1244        let string = this.to_string(context)?;
1245        Ok(JsValue::new(string.trim_matches(is_trimmable_whitespace)))
1246    }
1247
1248    /// `String.prototype.trimStart()`
1249    ///
1250    /// The `trimStart()` method removes whitespace from the beginning of a string.
1251    ///
1252    /// Whitespace in this context is all the whitespace characters (space, tab, no-break space, etc.) and all the line terminator characters (LF, CR, etc.).
1253    ///
1254    /// More information:
1255    ///  - [ECMAScript reference][spec]
1256    ///  - [MDN documentation][mdn]
1257    ///
1258    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.trimstart
1259    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trimStart
1260    pub(crate) fn trim_start(
1261        this: &JsValue,
1262        _: &[JsValue],
1263        context: &mut Context,
1264    ) -> JsResult<JsValue> {
1265        let string = this.to_string(context)?;
1266        Ok(JsValue::new(
1267            string.trim_start_matches(is_trimmable_whitespace),
1268        ))
1269    }
1270
1271    /// String.prototype.trimEnd()
1272    ///
1273    /// The `trimEnd()` method removes whitespace from the end of a string.
1274    ///
1275    /// Whitespace in this context is all the whitespace characters (space, tab, no-break space, etc.) and all the line terminator characters (LF, CR, etc.).
1276    ///
1277    /// More information:
1278    ///  - [ECMAScript reference][spec]
1279    ///  - [MDN documentation][mdn]
1280    ///
1281    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.trimend
1282    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trimEnd
1283    pub(crate) fn trim_end(
1284        this: &JsValue,
1285        _: &[JsValue],
1286        context: &mut Context,
1287    ) -> JsResult<JsValue> {
1288        let this = this.require_object_coercible(context)?;
1289        let string = this.to_string(context)?;
1290        Ok(JsValue::new(
1291            string.trim_end_matches(is_trimmable_whitespace),
1292        ))
1293    }
1294
1295    /// `String.prototype.toLowerCase()`
1296    ///
1297    /// The `toLowerCase()` method returns the calling string value converted to lower case.
1298    ///
1299    /// More information:
1300    ///  - [ECMAScript reference][spec]
1301    ///  - [MDN documentation][mdn]
1302    ///
1303    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.tolowercase
1304    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toLowerCase
1305    #[allow(clippy::wrong_self_convention)]
1306    pub(crate) fn to_lowercase(
1307        this: &JsValue,
1308        _: &[JsValue],
1309        context: &mut Context,
1310    ) -> JsResult<JsValue> {
1311        // First we get it the actual string a private field stored on the object only the context has access to.
1312        // Then we convert it into a Rust String by wrapping it in from_value
1313        let this_str = this.to_string(context)?;
1314        // The Rust String is mapped to uppercase using the builtin .to_lowercase().
1315        // There might be corner cases where it does not behave exactly like Javascript expects
1316        Ok(JsValue::new(this_str.to_lowercase()))
1317    }
1318
1319    /// `String.prototype.toUpperCase()`
1320    ///
1321    /// The `toUpperCase()` method returns the calling string value converted to uppercase.
1322    ///
1323    /// The value will be **converted** to a string if it isn't one
1324    ///
1325    /// More information:
1326    ///  - [ECMAScript reference][spec]
1327    ///  - [MDN documentation][mdn]
1328    ///
1329    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.toUppercase
1330    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toUpperCase
1331    #[allow(clippy::wrong_self_convention)]
1332    pub(crate) fn to_uppercase(
1333        this: &JsValue,
1334        _: &[JsValue],
1335        context: &mut Context,
1336    ) -> JsResult<JsValue> {
1337        // First we get it the actual string a private field stored on the object only the context has access to.
1338        // Then we convert it into a Rust String by wrapping it in from_value
1339        let this_str = this.to_string(context)?;
1340        // The Rust String is mapped to uppercase using the builtin .to_uppercase().
1341        // There might be corner cases where it does not behave exactly like Javascript expects
1342        Ok(JsValue::new(this_str.to_uppercase()))
1343    }
1344
1345    /// `String.prototype.substring( indexStart[, indexEnd] )`
1346    ///
1347    /// The `substring()` method returns the part of the `string` between the start and end indexes, or to the end of the string.
1348    ///
1349    /// More information:
1350    ///  - [ECMAScript reference][spec]
1351    ///  - [MDN documentation][mdn]
1352    ///
1353    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.substring
1354    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/substring
1355    pub(crate) fn substring(
1356        this: &JsValue,
1357        args: &[JsValue],
1358        context: &mut Context,
1359    ) -> JsResult<JsValue> {
1360        // First we get it the actual string a private field stored on the object only the context has access to.
1361        // Then we convert it into a Rust String by wrapping it in from_value
1362        let primitive_val = this.to_string(context)?;
1363        // If no args are specified, start is 'undefined', defaults to 0
1364        let start = if let Some(integer) = args.get(0) {
1365            integer.to_integer(context)? as i32
1366        } else {
1367            0
1368        };
1369        let length = primitive_val.encode_utf16().count() as i32;
1370        // If less than 2 args specified, end is the length of the this object converted to a String
1371        let end = if let Some(integer) = args.get(1) {
1372            integer.to_integer(context)? as i32
1373        } else {
1374            length
1375        };
1376        // Both start and end args replaced by 0 if they were negative
1377        // or by the length of the String if they were greater
1378        let final_start = min(max(start, 0), length);
1379        let final_end = min(max(end, 0), length);
1380        // Start and end are swapped if start is greater than end
1381        let from = min(final_start, final_end) as usize;
1382        let to = max(final_start, final_end) as usize;
1383        // Extract the part of the string contained between the start index and the end index
1384        // where start is guaranteed to be smaller or equals to end
1385        let extracted_string: Result<StdString, _> = decode_utf16(
1386            primitive_val
1387                .encode_utf16()
1388                .skip(from)
1389                .take(to.wrapping_sub(from)),
1390        )
1391        .collect();
1392        Ok(JsValue::new(extracted_string.expect("Invalid string")))
1393    }
1394
1395    /// `String.prototype.substr( start[, length] )`
1396    ///
1397    /// The `substr()` method returns a portion of the string, starting at the specified index and extending for a given number of characters afterward.
1398    ///
1399    /// More information:
1400    ///  - [ECMAScript reference][spec]
1401    ///  - [MDN documentation][mdn]
1402    ///
1403    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.substr
1404    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/substr
1405    /// <https://tc39.es/ecma262/#sec-string.prototype.substr>
1406    pub(crate) fn substr(
1407        this: &JsValue,
1408        args: &[JsValue],
1409        context: &mut Context,
1410    ) -> JsResult<JsValue> {
1411        // First we get it the actual string a private field stored on the object only the context has access to.
1412        // Then we convert it into a Rust String by wrapping it in from_value
1413        let primitive_val = this.to_string(context)?;
1414        // If no args are specified, start is 'undefined', defaults to 0
1415        let mut start = if let Some(integer) = args.get(0) {
1416            integer.to_integer(context)? as i32
1417        } else {
1418            0
1419        };
1420        let length = primitive_val.chars().count() as i32;
1421        // If less than 2 args specified, end is +infinity, the maximum number value.
1422        // Using i32::max_value() should be safe because the final length used is at most
1423        // the number of code units from start to the end of the string,
1424        // which should always be smaller or equals to both +infinity and i32::max_value
1425        let end = if let Some(integer) = args.get(1) {
1426            integer.to_integer(context)? as i32
1427        } else {
1428            i32::MAX
1429        };
1430        // If start is negative it become the number of code units from the end of the string
1431        if start < 0 {
1432            start = max(length.wrapping_add(start), 0);
1433        }
1434        // length replaced by 0 if it was negative
1435        // or by the number of code units from start to the end of the string if it was greater
1436        let result_length = min(max(end, 0), length.wrapping_sub(start));
1437        // If length is negative we return an empty string
1438        // otherwise we extract the part of the string from start and is length code units long
1439        if result_length <= 0 {
1440            Ok(JsValue::new(""))
1441        } else {
1442            let extracted_string: StdString = primitive_val
1443                .chars()
1444                .skip(start as usize)
1445                .take(result_length as usize)
1446                .collect();
1447
1448            Ok(JsValue::new(extracted_string))
1449        }
1450    }
1451
1452    /// `String.prototype.split ( separator, limit )`
1453    ///
1454    /// The split() method divides a String into an ordered list of substrings, puts these substrings into an array, and returns the array.
1455    /// The division is done by searching for a pattern; where the pattern is provided as the first parameter in the method's call.
1456    ///
1457    /// More information:
1458    ///  - [ECMAScript reference][spec]
1459    ///  - [MDN documentation][mdn]
1460    ///
1461    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.split
1462    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split
1463    pub(crate) fn split(
1464        this: &JsValue,
1465        args: &[JsValue],
1466        context: &mut Context,
1467    ) -> JsResult<JsValue> {
1468        // 1. Let O be ? RequireObjectCoercible(this value).
1469        let this = this.require_object_coercible(context)?;
1470
1471        let separator = args.get_or_undefined(0);
1472        let limit = args.get_or_undefined(1);
1473
1474        // 2. If separator is neither undefined nor null, then
1475        if !separator.is_null_or_undefined() {
1476            // a. Let splitter be ? GetMethod(separator, @@split).
1477            // b. If splitter is not undefined, then
1478            if let Some(splitter) = separator
1479                .as_object()
1480                .unwrap_or_default()
1481                .get_method(context, WellKnownSymbols::split())?
1482            {
1483                // i. Return ? Call(splitter, separator, « O, limit »).
1484                return splitter.call(separator, &[this.clone(), limit.clone()], context);
1485            }
1486        }
1487
1488        // 3. Let S be ? ToString(O).
1489        let this_str = this.to_string(context)?;
1490
1491        // 4. Let A be ! ArrayCreate(0).
1492        let a = Array::array_create(0, None, context)?;
1493
1494        // 5. Let lengthA be 0.
1495        let mut length_a = 0;
1496
1497        // 6.  If limit is undefined, let lim be 2^32 - 1; else let lim be ℝ(? ToUint32(limit)).
1498        let lim = if limit.is_undefined() {
1499            u32::MAX
1500        } else {
1501            limit.to_u32(context)?
1502        };
1503
1504        // 7. Let R be ? ToString(separator).
1505        let separator_str = separator.to_string(context)?;
1506
1507        // 8. If lim = 0, return A.
1508        if lim == 0 {
1509            return Ok(a.into());
1510        }
1511
1512        // 9. If separator is undefined, then
1513        if separator.is_undefined() {
1514            // a. Perform ! CreateDataPropertyOrThrow(A, "0", S).
1515            a.create_data_property_or_throw(0, this_str, context)
1516                .unwrap();
1517
1518            // b. Return A.
1519            return Ok(a.into());
1520        }
1521
1522        // 10. Let s be the length of S.
1523        let this_str_length = this_str.encode_utf16().count();
1524
1525        // 11. If s = 0, then
1526        if this_str_length == 0 {
1527            // a. If R is not the empty String, then
1528            if !separator_str.is_empty() {
1529                // i. Perform ! CreateDataPropertyOrThrow(A, "0", S).
1530                a.create_data_property_or_throw(0, this_str, context)
1531                    .unwrap();
1532            }
1533
1534            // b. Return A.
1535            return Ok(a.into());
1536        }
1537
1538        // 12. Let p be 0.
1539        // 13. Let q be p.
1540        let mut p = 0;
1541        let mut q = p;
1542
1543        // 14. Repeat, while q ≠ s,
1544        while q != this_str_length {
1545            // a. Let e be SplitMatch(S, q, R).
1546            let e = split_match(&this_str, q, &separator_str);
1547
1548            match e {
1549                // b. If e is not-matched, set q to q + 1.
1550                None => q += 1,
1551                // c. Else,
1552                Some(e) => {
1553                    // i. Assert: e is a non-negative integer ≤ s.
1554                    // ii. If e = p, set q to q + 1.
1555                    // iii. Else,
1556                    if e == p {
1557                        q += 1;
1558                    } else {
1559                        // 1. Let T be the substring of S from p to q.
1560                        let this_str_substring = StdString::from_utf16_lossy(
1561                            &this_str
1562                                .encode_utf16()
1563                                .skip(p)
1564                                .take(q - p)
1565                                .collect::<Vec<u16>>(),
1566                        );
1567
1568                        // 2. Perform ! CreateDataPropertyOrThrow(A, ! ToString(𝔽(lengthA)), T).
1569                        a.create_data_property_or_throw(length_a, this_str_substring, context)
1570                            .unwrap();
1571
1572                        // 3. Set lengthA to lengthA + 1.
1573                        length_a += 1;
1574
1575                        // 4. If lengthA = lim, return A.
1576                        if length_a == lim {
1577                            return Ok(a.into());
1578                        }
1579
1580                        // 5. Set p to e.
1581                        p = e;
1582
1583                        // 6. Set q to p.
1584                        q = p;
1585                    }
1586                }
1587            }
1588        }
1589
1590        // 15. Let T be the substring of S from p to s.
1591        let this_str_substring = StdString::from_utf16_lossy(
1592            &this_str
1593                .encode_utf16()
1594                .skip(p)
1595                .take(this_str_length - p)
1596                .collect::<Vec<u16>>(),
1597        );
1598
1599        // 16. Perform ! CreateDataPropertyOrThrow(A, ! ToString(𝔽(lengthA)), T).
1600        a.create_data_property_or_throw(length_a, this_str_substring, context)
1601            .unwrap();
1602
1603        // 17. Return A.
1604        Ok(a.into())
1605    }
1606
1607    /// String.prototype.valueOf()
1608    ///
1609    /// The `valueOf()` method returns the primitive value of a `String` object.
1610    ///
1611    /// More information:
1612    ///  - [ECMAScript reference][spec]
1613    ///  - [MDN documentation][mdn]
1614    ///
1615    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.value_of
1616    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/valueOf
1617    pub(crate) fn value_of(
1618        this: &JsValue,
1619        args: &[JsValue],
1620        context: &mut Context,
1621    ) -> JsResult<JsValue> {
1622        // Use the to_string method because it is specified to do the same thing in this case
1623        Self::to_string(this, args, context)
1624    }
1625
1626    /// `String.prototype.matchAll( regexp )`
1627    ///
1628    /// The `matchAll()` method returns an iterator of all results matching a string against a [`regular expression`][regex], including [capturing groups][cg].
1629    ///
1630    /// More information:
1631    ///  - [ECMAScript reference][spec]
1632    ///  - [MDN documentation][mdn]
1633    ///
1634    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.matchall
1635    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/matchAll
1636    /// [regex]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
1637    /// [cg]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Groups_and_Ranges
1638    pub(crate) fn match_all(
1639        this: &JsValue,
1640        args: &[JsValue],
1641        context: &mut Context,
1642    ) -> JsResult<JsValue> {
1643        // 1. Let O be ? RequireObjectCoercible(this value).
1644        let o = this.require_object_coercible(context)?;
1645
1646        // 2. If regexp is neither undefined nor null, then
1647        let regexp = args.get_or_undefined(0);
1648        if !regexp.is_null_or_undefined() {
1649            // a. Let isRegExp be ? IsRegExp(regexp).
1650            // b. If isRegExp is true, then
1651            if regexp.as_object().unwrap_or_default().is_regexp() {
1652                // i. Let flags be ? Get(regexp, "flags").
1653                let flags = regexp.get_field("flags", context)?;
1654
1655                // ii. Perform ? RequireObjectCoercible(flags).
1656                flags.require_object_coercible(context)?;
1657
1658                // iii. If ? ToString(flags) does not contain "g", throw a TypeError exception.
1659                if !flags.to_string(context)?.contains('g') {
1660                    return context.throw_type_error(
1661                        "String.prototype.matchAll called with a non-global RegExp argument",
1662                    );
1663                }
1664            }
1665
1666            // c. Let matcher be ? GetMethod(regexp, @@matchAll).
1667            // d. If matcher is not undefined, then
1668            if let Some(obj) = regexp.as_object() {
1669                if let Some(matcher) = obj.get_method(context, WellKnownSymbols::match_all())? {
1670                    // i. Return ? Call(matcher, regexp, « O »).
1671                    return matcher.call(regexp, &[o.clone()], context);
1672                }
1673            }
1674        }
1675
1676        // 3. Let S be ? ToString(O).
1677        let s = o.to_string(context)?;
1678
1679        // 4. Let rx be ? RegExpCreate(regexp, "g").
1680        let rx = RegExp::create(regexp.clone(), JsValue::new("g"), context)?;
1681
1682        // 5. Return ? Invoke(rx, @@matchAll, « S »).
1683        let obj = rx.as_object().expect("RegExpCreate must return Object");
1684        if let Some(matcher) = obj.get_method(context, WellKnownSymbols::match_all())? {
1685            matcher.call(&rx, &[JsValue::new(s)], context)
1686        } else {
1687            context.throw_type_error("RegExp[Symbol.matchAll] is undefined")
1688        }
1689    }
1690
1691    /// `String.prototype.normalize( [ form ] )`
1692    ///
1693    /// The normalize() method normalizes a string into a form specified in the Unicode® Standard Annex #15
1694    ///
1695    /// More information:
1696    ///  - [ECMAScript reference][spec]
1697    ///  - [MDN documentation][mdn]
1698    ///
1699    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.normalize
1700    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/normalize
1701    pub(crate) fn normalize(
1702        this: &JsValue,
1703        args: &[JsValue],
1704        context: &mut Context,
1705    ) -> JsResult<JsValue> {
1706        let this = this.require_object_coercible(context)?;
1707        let s = this.to_string(context)?;
1708        let form = args.get_or_undefined(0);
1709
1710        let f_str;
1711
1712        let f = if form.is_undefined() {
1713            "NFC"
1714        } else {
1715            f_str = form.to_string(context)?;
1716            f_str.as_str()
1717        };
1718
1719        match f {
1720            "NFC" => Ok(JsValue::new(s.nfc().collect::<StdString>())),
1721            "NFD" => Ok(JsValue::new(s.nfd().collect::<StdString>())),
1722            "NFKC" => Ok(JsValue::new(s.nfkc().collect::<StdString>())),
1723            "NFKD" => Ok(JsValue::new(s.nfkd().collect::<StdString>())),
1724            _ => context
1725                .throw_range_error("The normalization form should be one of NFC, NFD, NFKC, NFKD."),
1726        }
1727    }
1728
1729    /// `String.prototype.search( regexp )`
1730    ///
1731    /// The search() method executes a search for a match between a regular expression and this String object.
1732    ///
1733    /// More information:
1734    ///  - [ECMAScript reference][spec]
1735    ///  - [MDN documentation][mdn]
1736    ///
1737    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.search
1738    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/search
1739    pub(crate) fn search(
1740        this: &JsValue,
1741        args: &[JsValue],
1742        context: &mut Context,
1743    ) -> JsResult<JsValue> {
1744        // 1. Let O be ? RequireObjectCoercible(this value).
1745        let o = this.require_object_coercible(context)?;
1746
1747        // 2. If regexp is neither undefined nor null, then
1748        let regexp = args.get_or_undefined(0);
1749        if !regexp.is_null_or_undefined() {
1750            // a. Let searcher be ? GetMethod(regexp, @@search).
1751            // b. If searcher is not undefined, then
1752            if let Some(obj) = regexp.as_object() {
1753                if let Some(searcher) = obj.get_method(context, WellKnownSymbols::search())? {
1754                    // i. Return ? Call(searcher, regexp, « O »).
1755                    return searcher.call(regexp, &[o.clone()], context);
1756                }
1757            }
1758        }
1759
1760        // 3. Let string be ? ToString(O).
1761        let string = o.to_string(context)?;
1762
1763        // 4. Let rx be ? RegExpCreate(regexp, undefined).
1764        let rx = RegExp::create(regexp.clone(), JsValue::undefined(), context)?;
1765
1766        // 5. Return ? Invoke(rx, @@search, « string »).
1767        let obj = rx.as_object().expect("RegExpCreate must return Object");
1768        if let Some(matcher) = obj.get_method(context, WellKnownSymbols::search())? {
1769            matcher.call(&rx, &[JsValue::new(string)], context)
1770        } else {
1771            context.throw_type_error("RegExp[Symbol.search] is undefined")
1772        }
1773    }
1774
1775    pub(crate) fn iterator(
1776        this: &JsValue,
1777        _: &[JsValue],
1778        context: &mut Context,
1779    ) -> JsResult<JsValue> {
1780        StringIterator::create_string_iterator(this.clone(), context)
1781    }
1782}
1783
1784/// `22.1.3.17.1 GetSubstitution ( matched, str, position, captures, namedCaptures, replacement )`
1785///
1786/// More information:
1787///  - [ECMAScript reference][spec]
1788///
1789/// [spec]: https://tc39.es/ecma262/#sec-getsubstitution
1790pub(crate) fn get_substitution(
1791    matched: StdString,
1792    str: StdString,
1793    position: usize,
1794    captures: Vec<JsValue>,
1795    named_captures: JsValue,
1796    replacement: JsString,
1797    context: &mut Context,
1798) -> JsResult<JsString> {
1799    // 1. Assert: Type(matched) is String.
1800
1801    // 2. Let matchLength be the number of code units in matched.
1802    let match_length = matched.encode_utf16().count();
1803
1804    // 3. Assert: Type(str) is String.
1805
1806    // 4. Let stringLength be the number of code units in str.
1807    let str_length = str.encode_utf16().count();
1808
1809    // 5. Assert: position ≤ stringLength.
1810    // 6. Assert: captures is a possibly empty List of Strings.
1811    // 7. Assert: Type(replacement) is String.
1812
1813    // 8. Let tailPos be position + matchLength.
1814    let tail_pos = position + match_length;
1815
1816    // 9. Let m be the number of elements in captures.
1817    let m = captures.len();
1818
1819    // 10. Let result be the String value derived from replacement by copying code unit elements
1820    //     from replacement to result while performing replacements as specified in Table 58.
1821    //     These $ replacements are done left-to-right, and, once such a replacement is performed,
1822    //     the new replacement text is not subject to further replacements.
1823    let mut result = StdString::new();
1824    let mut chars = replacement.chars().peekable();
1825
1826    while let Some(first) = chars.next() {
1827        if first == '$' {
1828            let second = chars.next();
1829            let second_is_digit = second.map_or(false, |ch| ch.is_digit(10));
1830            // we use peek so that it is still in the iterator if not used
1831            let third = if second_is_digit { chars.peek() } else { None };
1832            let third_is_digit = third.map_or(false, |ch| ch.is_digit(10));
1833
1834            match (second, third) {
1835                // $$
1836                (Some('$'), _) => {
1837                    // $
1838                    result.push('$');
1839                }
1840                // $&
1841                (Some('&'), _) => {
1842                    // matched
1843                    result.push_str(&matched);
1844                }
1845                // $`
1846                (Some('`'), _) => {
1847                    // The replacement is the substring of str from 0 to position.
1848                    result.push_str(&StdString::from_utf16_lossy(
1849                        &str.encode_utf16().take(position).collect::<Vec<u16>>(),
1850                    ));
1851                }
1852                // $'
1853                (Some('\''), _) => {
1854                    // If tailPos ≥ stringLength, the replacement is the empty String.
1855                    // Otherwise the replacement is the substring of str from tailPos.
1856                    if tail_pos < str_length {
1857                        result.push_str(&StdString::from_utf16_lossy(
1858                            &str.encode_utf16().skip(tail_pos).collect::<Vec<u16>>(),
1859                        ));
1860                    }
1861                }
1862                // $nn
1863                (Some(second), Some(third)) if second_is_digit && third_is_digit => {
1864                    // The nnth element of captures, where nn is a two-digit decimal number in the range 01 to 99.
1865                    let tens = second.to_digit(10).unwrap() as usize;
1866                    let units = third.to_digit(10).unwrap() as usize;
1867                    let nn = 10 * tens + units;
1868
1869                    // If nn ≤ m and the nnth element of captures is undefined, use the empty String instead.
1870                    // If nn is 00 or nn > m, no replacement is done.
1871                    if nn == 0 || nn > m {
1872                        result.push('$');
1873                        result.push(second);
1874                        result.push(*third);
1875                    } else if let Some(capture) = captures.get(nn - 1) {
1876                        if let Some(s) = capture.as_string() {
1877                            result.push_str(s);
1878                        }
1879                    }
1880
1881                    chars.next();
1882                }
1883                // $n
1884                (Some(second), _) if second_is_digit => {
1885                    // The nth element of captures, where n is a single digit in the range 1 to 9.
1886                    let n = second.to_digit(10).unwrap() as usize;
1887
1888                    // If n ≤ m and the nth element of captures is undefined, use the empty String instead.
1889                    // If n > m, no replacement is done.
1890                    if n == 0 || n > m {
1891                        result.push('$');
1892                        result.push(second);
1893                    } else if let Some(capture) = captures.get(n - 1) {
1894                        if let Some(s) = capture.as_string() {
1895                            result.push_str(s);
1896                        }
1897                    }
1898                }
1899                // $<
1900                (Some('<'), _) => {
1901                    // 1. If namedCaptures is undefined, the replacement text is the String "$<".
1902                    // 2. Else,
1903                    if named_captures.is_undefined() {
1904                        result.push_str("$<")
1905                    } else {
1906                        // a. Assert: Type(namedCaptures) is Object.
1907
1908                        // b. Scan until the next > U+003E (GREATER-THAN SIGN).
1909                        let mut group_name = StdString::new();
1910                        let mut found = false;
1911                        loop {
1912                            match chars.next() {
1913                                Some('>') => {
1914                                    found = true;
1915                                    break;
1916                                }
1917                                Some(c) => group_name.push(c),
1918                                None => break,
1919                            }
1920                        }
1921
1922                        // c. If none is found, the replacement text is the String "$<".
1923                        // d. Else,
1924                        if !found {
1925                            result.push_str("$<");
1926                            result.push_str(&group_name);
1927                        } else {
1928                            // i. Let groupName be the enclosed substring.
1929                            // ii. Let capture be ? Get(namedCaptures, groupName).
1930                            let capture = named_captures.get_field(group_name, context)?;
1931
1932                            // iii. If capture is undefined, replace the text through > with the empty String.
1933                            // iv. Otherwise, replace the text through > with ? ToString(capture).
1934                            if !capture.is_undefined() {
1935                                result.push_str(capture.to_string(context)?.as_str());
1936                            }
1937                        }
1938                    }
1939                }
1940                // $?, ? is none of the above
1941                _ => {
1942                    result.push('$');
1943                    if let Some(second) = second {
1944                        result.push(second);
1945                    }
1946                }
1947            }
1948        } else {
1949            result.push(first);
1950        }
1951    }
1952
1953    // 11. Return result.
1954    Ok(result.into())
1955}
1956
1957/// `22.1.3.21.1 SplitMatch ( S, q, R )`
1958///
1959/// More information:
1960///  - [ECMAScript reference][spec]
1961///
1962/// [spec]: https://tc39.es/ecma262/#sec-splitmatch
1963fn split_match(s_str: &str, q: usize, r_str: &str) -> Option<usize> {
1964    // 1. Let r be the number of code units in R.
1965    let r = r_str.encode_utf16().count();
1966
1967    // 2. Let s be the number of code units in S.
1968    let s = s_str.encode_utf16().count();
1969
1970    // 3. If q + r > s, return not-matched.
1971    if q + r > s {
1972        return None;
1973    }
1974
1975    // 4. If there exists an integer i between 0 (inclusive) and r (exclusive)
1976    //    such that the code unit at index q + i within S is different from the code unit at index i within R,
1977    //    return not-matched.
1978    for i in 0..r {
1979        if let Some(s_char) = s_str.encode_utf16().nth(q + i) {
1980            if let Some(r_char) = r_str.encode_utf16().nth(i) {
1981                if s_char != r_char {
1982                    return None;
1983                }
1984            }
1985        }
1986    }
1987
1988    // 5. Return q + r.
1989    Some(q + r)
1990}