Skip to main content

boa_engine/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
12use crate::{
13    Context, JsArgs, JsExpect, JsResult, JsString, JsValue,
14    builtins::{Array, BuiltInObject, Number, RegExp},
15    context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors},
16    error::JsNativeError,
17    js_string,
18    object::{JsObject, internal_methods::get_prototype_from_constructor},
19    property::{Attribute, PropertyDescriptor},
20    realm::Realm,
21    string::{CodePoint, StaticJsStrings},
22    symbol::JsSymbol,
23    value::IntegerOrInfinity,
24};
25use boa_macros::utf16;
26
27use cow_utils::CowUtils;
28use icu_normalizer::{ComposingNormalizer, DecomposingNormalizer};
29use std::{
30    borrow::Cow,
31    cmp::{max, min},
32};
33
34use super::{BuiltInBuilder, BuiltInConstructor, IntrinsicObject};
35
36mod string_iterator;
37pub(crate) use string_iterator::StringIterator;
38
39#[cfg(feature = "annex-b")]
40pub use crate::{JsStr, js_str};
41
42/// The set of normalizers required for the `String.prototype.normalize` function.
43#[derive(Debug)]
44pub(crate) struct StringNormalizers {
45    pub(crate) nfc: ComposingNormalizer,
46    pub(crate) nfkc: ComposingNormalizer,
47    pub(crate) nfd: DecomposingNormalizer,
48    pub(crate) nfkd: DecomposingNormalizer,
49}
50
51#[cfg(test)]
52mod tests;
53
54#[derive(Clone, Copy, Eq, PartialEq)]
55pub(crate) enum Placement {
56    Start,
57    End,
58}
59
60/// JavaScript `String` implementation.
61#[derive(Debug, Clone, Copy)]
62pub(crate) struct String;
63
64impl IntrinsicObject for String {
65    fn init(realm: &Realm) {
66        let trim_start = BuiltInBuilder::callable(realm, Self::trim_start)
67            .length(0)
68            .name(js_string!("trimStart"))
69            .build();
70
71        let trim_end = BuiltInBuilder::callable(realm, Self::trim_end)
72            .length(0)
73            .name(js_string!("trimEnd"))
74            .build();
75
76        #[cfg(feature = "annex-b")]
77        let trim_left = trim_start.clone();
78
79        #[cfg(feature = "annex-b")]
80        let trim_right = trim_end.clone();
81
82        let attribute = Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::PERMANENT;
83        let builder = BuiltInBuilder::from_standard_constructor::<Self>(realm)
84            .property(js_string!("length"), 0, attribute)
85            .property(
86                js_string!("trimStart"),
87                trim_start,
88                Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
89            )
90            .property(
91                js_string!("trimEnd"),
92                trim_end,
93                Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
94            )
95            .static_method(Self::raw, js_string!("raw"), 1)
96            .static_method(Self::from_char_code, js_string!("fromCharCode"), 1)
97            .static_method(Self::from_code_point, js_string!("fromCodePoint"), 1)
98            .method(Self::char_at, js_string!("charAt"), 1)
99            .method(Self::char_code_at, js_string!("charCodeAt"), 1)
100            .method(Self::code_point_at, js_string!("codePointAt"), 1)
101            .method(Self::to_string, js_string!("toString"), 0)
102            .method(Self::concat, js_string!("concat"), 1)
103            .method(Self::repeat, js_string!("repeat"), 1)
104            .method(Self::slice, js_string!("slice"), 2)
105            .method(Self::starts_with, js_string!("startsWith"), 1)
106            .method(Self::ends_with, js_string!("endsWith"), 1)
107            .method(Self::includes, js_string!("includes"), 1)
108            .method(Self::index_of, js_string!("indexOf"), 1)
109            .method(Self::is_well_formed, js_string!("isWellFormed"), 0)
110            .method(Self::last_index_of, js_string!("lastIndexOf"), 1)
111            .method(Self::locale_compare, js_string!("localeCompare"), 1)
112            .method(Self::r#match, js_string!("match"), 1)
113            .method(Self::normalize, js_string!("normalize"), 0)
114            .method(Self::pad_end, js_string!("padEnd"), 1)
115            .method(Self::pad_start, js_string!("padStart"), 1)
116            .method(Self::trim, js_string!("trim"), 0)
117            .method(Self::to_case::<false>, js_string!("toLowerCase"), 0)
118            .method(Self::to_case::<true>, js_string!("toUpperCase"), 0)
119            .method(Self::to_well_formed, js_string!("toWellFormed"), 0)
120            .method(
121                Self::to_locale_case::<false>,
122                js_string!("toLocaleLowerCase"),
123                0,
124            )
125            .method(
126                Self::to_locale_case::<true>,
127                js_string!("toLocaleUpperCase"),
128                0,
129            )
130            .method(Self::substring, js_string!("substring"), 2)
131            .method(Self::split, js_string!("split"), 2)
132            .method(Self::value_of, js_string!("valueOf"), 0)
133            .method(Self::match_all, js_string!("matchAll"), 1)
134            .method(Self::replace, js_string!("replace"), 2)
135            .method(Self::replace_all, js_string!("replaceAll"), 2)
136            .method(Self::iterator, JsSymbol::iterator(), 0)
137            .method(Self::search, js_string!("search"), 1)
138            .method(Self::at, js_string!("at"), 1);
139
140        #[cfg(feature = "annex-b")]
141        let builder = {
142            builder
143                .property(
144                    js_string!("trimLeft"),
145                    trim_left,
146                    Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
147                )
148                .property(
149                    js_string!("trimRight"),
150                    trim_right,
151                    Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
152                )
153                .method(Self::substr, js_string!("substr"), 2)
154                .method(Self::anchor, js_string!("anchor"), 1)
155                .method(Self::big, js_string!("big"), 0)
156                .method(Self::blink, js_string!("blink"), 0)
157                .method(Self::bold, js_string!("bold"), 0)
158                .method(Self::fixed, js_string!("fixed"), 0)
159                .method(Self::fontcolor, js_string!("fontcolor"), 1)
160                .method(Self::fontsize, js_string!("fontsize"), 1)
161                .method(Self::italics, js_string!("italics"), 0)
162                .method(Self::link, js_string!("link"), 1)
163                .method(Self::small, js_string!("small"), 0)
164                .method(Self::strike, js_string!("strike"), 0)
165                .method(Self::sub, js_string!("sub"), 0)
166                .method(Self::sup, js_string!("sup"), 0)
167        };
168
169        builder.build();
170    }
171
172    fn get(intrinsics: &Intrinsics) -> JsObject {
173        Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor()
174    }
175}
176
177impl BuiltInObject for String {
178    const NAME: JsString = StaticJsStrings::STRING;
179}
180
181impl BuiltInConstructor for String {
182    const CONSTRUCTOR_ARGUMENTS: usize = 1;
183    const PROTOTYPE_STORAGE_SLOTS: usize = 52;
184    const CONSTRUCTOR_STORAGE_SLOTS: usize = 3;
185
186    const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor =
187        StandardConstructors::string;
188
189    /// Constructor `String( value )`
190    ///
191    /// <https://tc39.es/ecma262/#sec-string-constructor-string-value>
192    fn constructor(
193        new_target: &JsValue,
194        args: &[JsValue],
195        context: &mut Context,
196    ) -> JsResult<JsValue> {
197        // This value is used by console.log and other routines to match Object type
198        // to its Javascript Identifier (global constructor method name)
199        let string = match args.first() {
200            // 2. Else,
201            // a. If NewTarget is undefined and Type(value) is Symbol, return SymbolDescriptiveString(value).
202            Some(value) if new_target.is_undefined() && value.is_symbol() => {
203                return Ok(value
204                    .as_symbol()
205                    .expect("Already checked for a symbol")
206                    .descriptive_string()
207                    .into());
208            }
209            // b. Let s be ? ToString(value).
210            Some(value) => value.to_string(context)?,
211            // 1. If value is not present, let s be the empty String.
212            None => js_string!(),
213        };
214
215        // 3. If NewTarget is undefined, return s.
216        if new_target.is_undefined() {
217            return Ok(string.into());
218        }
219
220        let prototype =
221            get_prototype_from_constructor(new_target, StandardConstructors::string, context)?;
222        // 4. Return ! StringCreate(s, ? GetPrototypeFromConstructor(NewTarget, "%String.prototype%")).
223        Ok(Self::string_create(string, prototype, context).into())
224    }
225}
226
227impl String {
228    /// JavaScript strings must be between `0` and less than positive `Infinity` and cannot be a negative number.
229    /// The range of allowed values can be described like this: `[0, +∞)`.
230    ///
231    /// The resulting string can also not be larger than the maximum string size,
232    /// which can differ in JavaScript engines. In Boa it is `2^32 - 1`
233    pub(crate) const MAX_STRING_LENGTH: usize = u32::MAX as usize;
234
235    /// Abstract function `StringCreate( value, prototype )`.
236    ///
237    /// Call this function if you want to create a `String` exotic object.
238    ///
239    /// More information:
240    ///  - [ECMAScript reference][spec]
241    ///
242    /// [spec]: https://tc39.es/ecma262/#sec-stringcreate
243    fn string_create(value: JsString, prototype: JsObject, context: &mut Context) -> JsObject {
244        // 7. Let length be the number of code unit elements in value.
245        let len = value.len();
246
247        // 1. Let S be ! MakeBasicObject(« [[Prototype]], [[Extensible]], [[StringData]] »).
248        // 2. Set S.[[Prototype]] to prototype.
249        // 3. Set S.[[StringData]] to value.
250        // 4. Set S.[[GetOwnProperty]] as specified in 10.4.3.1.
251        // 5. Set S.[[DefineOwnProperty]] as specified in 10.4.3.2.
252        // 6. Set S.[[OwnPropertyKeys]] as specified in 10.4.3.3.
253        let s =
254            JsObject::from_proto_and_data_with_shared_shape(context.root_shape(), prototype, value)
255                .upcast();
256
257        // 8. Perform ! DefinePropertyOrThrow(S, "length", PropertyDescriptor { [[Value]]: 𝔽(length),
258        // [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: false }).
259        s.define_property_or_throw(
260            StaticJsStrings::LENGTH,
261            PropertyDescriptor::builder()
262                .value(len)
263                .writable(false)
264                .enumerable(false)
265                .configurable(false),
266            context,
267        )
268        .expect("length definition for a new string must not fail");
269
270        // 9. Return S.
271        s
272    }
273
274    /// Abstract operation `thisStringValue( value )`
275    ///
276    /// More information:
277    /// - [ECMAScript reference][spec]
278    ///
279    /// [spec]: https://tc39.es/ecma262/#thisstringvalue
280    fn this_string_value(this: &JsValue) -> JsResult<JsString> {
281        // 1. If Type(value) is String, return value.
282        this.as_string()
283            // 2. If Type(value) is Object and value has a [[StringData]] internal slot, then
284            //     a. Let s be value.[[StringData]].
285            //     b. Assert: Type(s) is String.
286            //     c. Return s.
287            .or_else(|| {
288                this.as_object()
289                    .and_then(|obj| obj.downcast_ref::<JsString>().as_deref().cloned())
290            })
291            // 3. Throw a TypeError exception.
292            .ok_or_else(|| {
293                JsNativeError::typ()
294                    .with_message("'this' is not a string")
295                    .into()
296            })
297    }
298
299    /// `String.fromCodePoint(num1[, ...[, numN]])`
300    ///
301    /// The static `String.fromCodePoint()` method returns a string created by using the specified sequence of code points.
302    ///
303    /// More information:
304    ///  - [ECMAScript reference][spec]
305    ///  - [MDN documentation][mdn]
306    ///
307    /// [spec]: https://tc39.es/ecma262/#sec-string.fromcodepoint
308    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/fromCodePoint
309    pub(crate) fn from_code_point(
310        _: &JsValue,
311        args: &[JsValue],
312        context: &mut Context,
313    ) -> JsResult<JsValue> {
314        // 1. Let result be the empty String.
315        let mut result = Vec::with_capacity(args.len());
316
317        let mut buf = [0; 2];
318
319        // 2. For each element next of codePoints, do
320        for arg in args {
321            // a. Let nextCP be ? ToNumber(next).
322            let nextcp = arg.to_number(context)?;
323
324            // b. If ! IsIntegralNumber(nextCP) is false, throw a RangeError exception.
325            if !Number::is_float_integer(nextcp) {
326                return Err(JsNativeError::range()
327                    .with_message(format!("codepoint `{nextcp}` is not an integer"))
328                    .into());
329            }
330
331            // c. If ℝ(nextCP) < 0 or ℝ(nextCP) > 0x10FFFF, throw a RangeError exception.
332            if nextcp < 0.0 || nextcp > f64::from(0x0010_FFFF) {
333                return Err(JsNativeError::range()
334                    .with_message(format!("codepoint `{nextcp}` outside of Unicode range"))
335                    .into());
336            }
337
338            // SAFETY:
339            // - `nextcp` is not NaN (by the call to `is_float_integer`).
340            // - `nextcp` is not infinite (by the call to `is_float_integer`).
341            // - `nextcp` is in the u32 range (by the check above).
342            let nextcp = unsafe { nextcp.to_int_unchecked::<u32>() };
343
344            // d. Set result to the string-concatenation of result and ! UTF16EncodeCodePoint(ℝ(nextCP)).
345            result.extend_from_slice(match u16::try_from(nextcp) {
346                Ok(ref cp) => std::slice::from_ref(cp),
347                Err(_) => char::from_u32(nextcp)
348                    .expect("u32 is in range and cannot be a surrogate by the conversion above")
349                    .encode_utf16(&mut buf),
350            });
351        }
352
353        // 3. Assert: If codePoints is empty, then result is the empty String.
354        // 4. Return result.
355        Ok(js_string!(&result[..]).into())
356    }
357
358    /// `String.raw( template, ...substitutions )`
359    ///
360    /// More information:
361    ///  - [ECMAScript reference][spec]
362    ///  - [MDN documentation][mdn]
363    ///
364    /// [spec]: https://tc39.es/ecma262/#sec-string.raw
365    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/raw
366    pub(crate) fn raw(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
367        let substitutions = args.get(1..).unwrap_or_default();
368
369        // 1. Let numberOfSubstitutions be the number of elements in substitutions.
370        let number_of_substitutions = substitutions.len() as u64;
371
372        // 2. Let cooked be ? ToObject(template).
373        let cooked = args.get_or_undefined(0).to_object(context)?;
374
375        // 3. Let raw be ? ToObject(? Get(cooked, "raw")).
376        let raw = cooked.get(js_string!("raw"), context)?.to_object(context)?;
377
378        // 4. Let literalSegments be ? LengthOfArrayLike(raw).
379        let literal_segments = raw.length_of_array_like(context)?;
380
381        // 5. If literalSegments ≤ 0, return the empty String.
382        // This is not <= because a `usize` is always positive.
383        if literal_segments == 0 {
384            return Ok(js_string!().into());
385        }
386
387        // 6. Let stringElements be a new empty List.
388        let mut string_elements = vec![];
389
390        // 7. Let nextIndex be 0.
391        let mut next_index = 0;
392        // 8. Repeat,
393        loop {
394            // a. Let nextKey be ! ToString(𝔽(nextIndex)).
395            let next_key = next_index;
396
397            // b. Let nextSeg be ? ToString(? Get(raw, nextKey)).
398            let next_seg = raw.get(next_key, context)?.to_string(context)?;
399
400            // c. Append the code unit elements of nextSeg to the end of stringElements.
401            string_elements.extend(next_seg.iter());
402
403            // d. If nextIndex + 1 = literalSegments, then
404            if next_index + 1 == literal_segments {
405                // i. Return the String value whose code units are the elements in the List stringElements.
406                //    If stringElements has no elements, the empty String is returned.
407                return Ok(js_string!(&string_elements[..]).into());
408            }
409
410            // e. If nextIndex < numberOfSubstitutions, let next be substitutions[nextIndex].
411            let next = if next_index < number_of_substitutions {
412                substitutions.get_or_undefined(next_index as usize).clone()
413
414            // f. Else, let next be the empty String.
415            } else {
416                js_string!().into()
417            };
418
419            // g. Let nextSub be ? ToString(next).
420            let next_sub = next.to_string(context)?;
421
422            // h. Append the code unit elements of nextSub to the end of stringElements.
423            string_elements.extend(next_sub.iter());
424
425            // i. Set nextIndex to nextIndex + 1.
426            next_index += 1;
427        }
428    }
429
430    /// `String.fromCharCode(...codeUnits)`
431    ///
432    /// Construct a `String` from one or more code points (as numbers).
433    /// More information:
434    /// - [ECMAScript reference][spec]
435    ///
436    /// [spec]: https://tc39.es/ecma262/multipage/text-processing.html#sec-string.fromcharcode
437    pub(crate) fn from_char_code(
438        _: &JsValue,
439        args: &[JsValue],
440        context: &mut Context,
441    ) -> JsResult<JsValue> {
442        // 1. Let result be the empty String.
443        let mut result = Vec::with_capacity(args.len());
444
445        // 2. For each element next of codeUnits, do
446        for next in args {
447            // a. Let nextCU be the code unit whose numeric value is ℝ(? ToUint16(next)).
448            let next_cu = next.to_uint16(context)?;
449
450            // b. Set result to the string-concatenation of result and nextCU.
451            result.push(next_cu);
452        }
453
454        // 3. Return result.
455        Ok(js_string!(&result[..]).into())
456    }
457
458    /// `String.prototype.toString ( )`
459    ///
460    /// More information:
461    /// - [ECMAScript reference][spec]
462    ///
463    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.tostring
464    #[allow(clippy::wrong_self_convention)]
465    pub(crate) fn to_string(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
466        // 1. Return ? thisStringValue(this value).
467        Ok(Self::this_string_value(this)?.into())
468    }
469
470    /// `String.prototype.charAt( index )`
471    ///
472    /// 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.
473    ///
474    /// Characters in a string are indexed from left to right. The index of the first character is `0`,
475    /// and the index of the last character—in a string called `stringName`—is `stringName.length - 1`.
476    /// If the `index` you supply is out of this range, JavaScript returns an empty string.
477    ///
478    /// If no index is provided to `charAt()`, the default is `0`.
479    ///
480    /// More information:
481    ///  - [ECMAScript reference][spec]
482    ///  - [MDN documentation][mdn]
483    ///
484    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.charat
485    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charAt
486    pub(crate) fn char_at(
487        this: &JsValue,
488        args: &[JsValue],
489        context: &mut Context,
490    ) -> JsResult<JsValue> {
491        // 1. Let O be ? RequireObjectCoercible(this value).
492        let this = this.require_object_coercible()?;
493
494        // 2. Let S be ? ToString(O).
495        let string = this.to_string(context)?;
496
497        // 3. Let position be ? ToIntegerOrInfinity(pos).
498        let position = args.get_or_undefined(0).to_integer_or_infinity(context)?;
499
500        match position {
501            // 4. Let size be the length of S.
502            // 6. Return the substring of S from position to position + 1.
503            IntegerOrInfinity::Integer(i) if i >= 0 && i < string.len() as i64 => {
504                let i = i as usize;
505                Ok(js_string!(string.get_expect(i..=i)).into())
506            }
507            // 5. If position < 0 or position ≥ size, return the empty String.
508            _ => Ok(js_string!().into()),
509        }
510    }
511
512    /// `String.prototype.at ( index )`
513    ///
514    /// This String object's `at()` method returns a String consisting of the single UTF-16 code unit located at the specified position.
515    /// Returns undefined if the given index cannot be found.
516    ///
517    /// More information:
518    ///  - [ECMAScript reference][spec]
519    ///  - [MDN documentation][mdn]
520    ///
521    /// [spec]: https://tc39.es/proposal-relative-indexing-method/#sec-string.prototype.at
522    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/at
523    pub(crate) fn at(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
524        // 1. Let O be ? RequireObjectCoercible(this value).
525        let this = this.require_object_coercible()?;
526
527        // 2. Let S be ? ToString(O).
528        let s = this.to_string(context)?;
529
530        // 3. Let len be the length of S.
531        let len = s.len() as i64;
532
533        // 4. Let relativeIndex be ? ToIntegerOrInfinity(index).
534        let relative_index = args.get_or_undefined(0).to_integer_or_infinity(context)?;
535        let k = match relative_index {
536            // 5. If relativeIndex ≥ 0, then
537            // a. Let k be relativeIndex.
538            IntegerOrInfinity::Integer(i) if i >= 0 && i < len => i as usize,
539            // 6. Else,
540            // a. Let k be len + relativeIndex.
541            IntegerOrInfinity::Integer(i) if i < 0 && (-i) <= len => (len + i) as usize,
542            // 7. If k < 0 or k ≥ len, return undefined.
543            _ => return Ok(JsValue::undefined()),
544        };
545
546        // 8. Return the substring of S from k to k + 1.
547        Ok(js_string!(s.get_expect(k..=k)).into())
548    }
549
550    /// `String.prototype.codePointAt( index )`
551    ///
552    /// The `codePointAt()` method returns an integer between `0` to `1114111` (`0x10FFFF`) representing the UTF-16 code unit at the given index.
553    ///
554    /// If no UTF-16 surrogate pair begins at the index, the code point at the index is returned.
555    ///
556    /// `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.
557    ///
558    /// More information:
559    ///  - [ECMAScript reference][spec]
560    ///  - [MDN documentation][mdn]
561    ///
562    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.codepointat
563    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/codePointAt
564    pub(crate) fn code_point_at(
565        this: &JsValue,
566        args: &[JsValue],
567        context: &mut Context,
568    ) -> JsResult<JsValue> {
569        // 1. Let O be ? RequireObjectCoercible(this value).
570        let this = this.require_object_coercible()?;
571
572        // 2. Let S be ? ToString(O).
573        let string = this.to_string(context)?;
574
575        // 3. Let position be ? ToIntegerOrInfinity(pos).
576        let position = args.get_or_undefined(0).to_integer_or_infinity(context)?;
577
578        match position {
579            // 4. Let size be the length of S.
580            IntegerOrInfinity::Integer(i) if i >= 0 && i < string.len() as i64 => {
581                // 6. Let cp be ! CodePointAt(S, position).
582                // 7. Return 𝔽(cp.[[CodePoint]]).
583                Ok(string
584                    .code_point_at(usize::try_from(i).expect("already checked that i >= 0"))
585                    .as_u32()
586                    .into())
587            }
588            // 5. If position < 0 or position ≥ size, return undefined.
589            _ => Ok(JsValue::undefined()),
590        }
591    }
592
593    /// `String.prototype.charCodeAt( index )`
594    ///
595    /// The `charCodeAt()` method returns an integer between `0` and `65535` representing the UTF-16 code unit at the given index.
596    ///
597    /// Unicode code points range from `0` to `1114111` (`0x10FFFF`). The first 128 Unicode code points are a direct match of the ASCII character encoding.
598    ///
599    /// `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.
600    ///
601    /// More information:
602    ///  - [ECMAScript reference][spec]
603    ///  - [MDN documentation][mdn]
604    ///
605    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.charcodeat
606    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt
607    pub(crate) fn char_code_at(
608        this: &JsValue,
609        args: &[JsValue],
610        context: &mut Context,
611    ) -> JsResult<JsValue> {
612        // 1. Let O be ? RequireObjectCoercible(this value).
613        let this = this.require_object_coercible()?;
614
615        // 2. Let S be ? ToString(O).
616        let string = this.to_string(context)?;
617
618        // 3. Let position be ? ToIntegerOrInfinity(pos).
619        let position = args.get_or_undefined(0).to_integer_or_infinity(context)?;
620
621        match position {
622            // 4. Let size be the length of S.
623            IntegerOrInfinity::Integer(i) if i >= 0 && i < string.len() as i64 => {
624                // 6. Return the Number value for the numeric value of the code unit at index position within the String S.
625                // SAFETY: already validated the index.
626                Ok(unsafe { string.code_unit_at(i as usize).unwrap_unchecked() }.into())
627            }
628            // 5. If position < 0 or position ≥ size, return NaN.
629            _ => Ok(JsValue::nan()),
630        }
631    }
632
633    /// `String.prototype.concat( str1[, ...strN] )`
634    ///
635    /// The `concat()` method concatenates the string arguments to the calling string and returns a new string.
636    ///
637    /// Changes to the original string or the returned string don't affect the other.
638    ///
639    /// If the arguments are not of the type string, they are converted to string values before concatenating.
640    ///
641    /// More information:
642    ///  - [ECMAScript reference][spec]
643    ///  - [MDN documentation][mdn]
644    ///
645    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.concat
646    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/concat
647    pub(crate) fn concat(
648        this: &JsValue,
649        args: &[JsValue],
650        context: &mut Context,
651    ) -> JsResult<JsValue> {
652        // 1. Let O be ? RequireObjectCoercible(this value).
653        let this = this.require_object_coercible()?;
654
655        // 2. Let S be ? ToString(O).
656        let mut string = this.to_string(context)?;
657
658        // 3. Let R be S.
659        // 4. For each element next of args, do
660        for arg in args {
661            // a. Let nextString be ? ToString(next).
662            // b. Set R to the string-concatenation of R and nextString.
663            string = js_string!(&string, &arg.to_string(context)?);
664        }
665
666        // 5. Return R.
667        Ok(JsValue::new(string))
668    }
669
670    /// `String.prototype.repeat( count )`
671    ///
672    /// The `repeat()` method constructs and returns a new string which contains the specified number of
673    /// copies of the string on which it was called, concatenated together.
674    ///
675    /// More information:
676    ///  - [ECMAScript reference][spec]
677    ///  - [MDN documentation][mdn]
678    ///
679    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.repeat
680    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat
681    pub(crate) fn repeat(
682        this: &JsValue,
683        args: &[JsValue],
684        context: &mut Context,
685    ) -> JsResult<JsValue> {
686        // 1. Let O be ? RequireObjectCoercible(this value).
687        let this = this.require_object_coercible()?;
688
689        // 2. Let S be ? ToString(O).
690        let string = this.to_string(context)?;
691
692        // 3. Let n be ? ToIntegerOrInfinity(count).
693        let n = args.get_or_undefined(0).to_integer_or_infinity(context)?;
694
695        // 4. If n < 0 or n is +∞, throw a RangeError exception.
696        let n = match n {
697            IntegerOrInfinity::Integer(i) if i < 0 => {
698                return Err(JsNativeError::range()
699                    .with_message("String.prototype.repeat: count must be non-negative")
700                    .into());
701            }
702            IntegerOrInfinity::PositiveInfinity => {
703                return Err(JsNativeError::range()
704                    .with_message("String.prototype.repeat: count must be less than infinity")
705                    .into());
706            }
707            IntegerOrInfinity::NegativeInfinity => {
708                return Err(JsNativeError::range()
709                    .with_message("String.prototype.repeat: count must be non-negative")
710                    .into());
711            }
712            IntegerOrInfinity::Integer(i) => i,
713        };
714
715        let len = string.len();
716
717        if n == 0 || string.is_empty() {
718            return Ok(js_string!().into());
719        }
720
721        if u64::try_from(n)
722            .ok()
723            .and_then(|n| n.checked_mul(len as u64))
724            .is_none_or(|total_len| total_len > (Self::MAX_STRING_LENGTH as u64))
725        {
726            return Err(JsNativeError::range()
727                .with_message(
728                    "repeat count must be a positive finite number \
729                        that doesn't overflow the maximum string length (2^32 - 1)",
730                )
731                .into());
732        }
733
734        let n = n as usize;
735
736        // Charge each repetition against the VM loop-iteration limit.
737        let mut result = Vec::with_capacity(n);
738        for _ in 0..n {
739            crate::vm::opcode::IncrementLoopIteration::operation((), context)?;
740            result.push(string.as_str());
741        }
742
743        // 6. Return the String value that is made from n copies of S appended together.
744        Ok(JsString::concat_array(&result).into())
745    }
746
747    /// `String.prototype.slice( beginIndex [, endIndex] )`
748    ///
749    /// The `slice()` method extracts a section of a string and returns it as a new string, without modifying the original string.
750    ///
751    /// More information:
752    ///  - [ECMAScript reference][spec]
753    ///  - [MDN documentation][mdn]
754    ///
755    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.slice
756    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/slice
757    pub(crate) fn slice(
758        this: &JsValue,
759        args: &[JsValue],
760        context: &mut Context,
761    ) -> JsResult<JsValue> {
762        // 1. Let O be ? RequireObjectCoercible(this value).
763        let this = this.require_object_coercible()?;
764
765        // 2. Let S be ? ToString(O).
766        let string = this.to_string(context)?;
767
768        // 3. Let len be the length of S.
769        let len = string.len() as i64;
770
771        // 4. Let intStart be ? ToIntegerOrInfinity(start).
772        let from = match args.get_or_undefined(0).to_integer_or_infinity(context)? {
773            // 6. Else if intStart < 0, let from be max(len + intStart, 0).
774            IntegerOrInfinity::Integer(i) if i < 0 => max(len + i, 0),
775
776            // 7. Else, let from be min(intStart, len).
777            IntegerOrInfinity::Integer(i) => min(i, len),
778            IntegerOrInfinity::PositiveInfinity => len,
779
780            // 5. If intStart is -∞, let from be 0.
781            IntegerOrInfinity::NegativeInfinity => 0,
782        } as usize;
783
784        // 8. If end is undefined, let intEnd be len; else let intEnd be ? ToIntegerOrInfinity(end).
785        let to = match args
786            .get(1)
787            .filter(|end| !end.is_undefined())
788            .map(|end| end.to_integer_or_infinity(context))
789            .transpose()?
790            .unwrap_or(IntegerOrInfinity::Integer(len))
791        {
792            // 10. Else if intEnd < 0, let to be max(len + intEnd, 0).
793            IntegerOrInfinity::Integer(i) if i < 0 => max(len + i, 0),
794
795            // 11. Else, let to be min(intEnd, len).
796            IntegerOrInfinity::Integer(i) => min(i, len),
797            IntegerOrInfinity::PositiveInfinity => len,
798
799            // 9. If intEnd is -∞, let to be 0.
800            IntegerOrInfinity::NegativeInfinity => 0,
801        } as usize;
802
803        // 12. If from ≥ to, return the empty String.
804        if from >= to {
805            Ok(js_string!().into())
806        } else {
807            // 13. Return the substring of S from from to to.
808            // SAFETY: We already checked that `from` and `to` are within bounds.
809            Ok(unsafe { JsString::slice_unchecked(&string, from, to).into() })
810        }
811    }
812
813    /// `String.prototype.startWith( searchString[, position] )`
814    ///
815    /// The `startsWith()` method determines whether a string begins with the characters of a specified string, returning `true` or `false` as appropriate.
816    ///
817    /// More information:
818    ///  - [ECMAScript reference][spec]
819    ///  - [MDN documentation][mdn]
820    ///
821    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.startswith
822    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith
823    pub(crate) fn starts_with(
824        this: &JsValue,
825        args: &[JsValue],
826        context: &mut Context,
827    ) -> JsResult<JsValue> {
828        // 1. Let O be ? RequireObjectCoercible(this value).
829        let this = this.require_object_coercible()?;
830
831        // 2. Let S be ? ToString(O).
832        let string = this.to_string(context)?;
833
834        let search_string = args.get_or_undefined(0);
835
836        // 3. Let isRegExp be ? IsRegExp(searchString).
837        // 4. If isRegExp is true, throw a TypeError exception.
838        if RegExp::is_reg_exp(search_string, context)?.is_some() {
839            return Err(JsNativeError::typ().with_message(
840                "First argument to String.prototype.startsWith must not be a regular expression",
841            ).into());
842        }
843
844        // 5. Let searchStr be ? ToString(searchString).
845        let search_string = search_string.to_string(context)?;
846
847        // 6. Let len be the length of S.
848        let len = string.len() as i64;
849
850        // 7. If position is undefined, let pos be 0; else let pos be ? ToIntegerOrInfinity(position).
851        let pos = args
852            .get_or_undefined(1)
853            .map_or(Ok(IntegerOrInfinity::Integer(0)), |pos| {
854                pos.to_integer_or_infinity(context)
855            })?;
856
857        // 8. Let start be the result of clamping pos between 0 and len.
858        let start = pos.clamp_finite(0, len) as usize;
859
860        // 9. Let searchLength be the length of searchStr.
861        let search_length = search_string.len();
862
863        // 10. If searchLength = 0, return true.
864        if search_length == 0 {
865            return Ok(JsValue::new(true));
866        }
867
868        // 11. Let end be start + searchLength.
869        let end = start + search_length;
870
871        // 12. If end > len, return false.
872        if end > len as usize {
873            Ok(JsValue::new(false))
874        } else {
875            // 13. Let substring be the substring of S from start to end.
876            // 14. Return ! SameValueNonNumeric(substring, searchStr).
877            // `SameValueNonNumeric` forwards to `==`, so directly check
878            // equality to avoid converting to `JsValue`
879            Ok(JsValue::new(search_string == string.get_expect(start..end)))
880        }
881    }
882
883    /// `String.prototype.endsWith( searchString[, length] )`
884    ///
885    /// The `endsWith()` method determines whether a string ends with the characters of a specified string, returning `true` or `false` as appropriate.
886    ///
887    /// More information:
888    ///  - [ECMAScript reference][spec]
889    ///  - [MDN documentation][mdn]
890    ///
891    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.endswith
892    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith
893    pub(crate) fn ends_with(
894        this: &JsValue,
895        args: &[JsValue],
896        context: &mut Context,
897    ) -> JsResult<JsValue> {
898        // 1. Let O be ? RequireObjectCoercible(this value).
899        let this = this.require_object_coercible()?;
900
901        // 2. Let S be ? ToString(O).
902        let string = this.to_string(context)?;
903
904        let search_str = match args.get_or_undefined(0) {
905            // 3. Let isRegExp be ? IsRegExp(searchString).
906            // 4. If isRegExp is true, throw a TypeError exception.
907            search_string if RegExp::is_reg_exp(search_string, context)?.is_some() => {
908                return Err(JsNativeError::typ().with_message(
909                    "First argument to String.prototype.endsWith must not be a regular expression",
910                ).into());
911            }
912            // 5. Let searchStr be ? ToString(searchString).
913            search_string => search_string.to_string(context)?,
914        };
915
916        // 6. Let len be the length of S.
917        let len = string.len() as i64;
918
919        // 7. If endPosition is undefined, let pos be len; else let pos be ? ToIntegerOrInfinity(endPosition).
920        let end = match args.get_or_undefined(1) {
921            end_position if end_position.is_undefined() => IntegerOrInfinity::Integer(len),
922            end_position => end_position.to_integer_or_infinity(context)?,
923        };
924
925        // 8. Let end be the result of clamping pos between 0 and len.
926        let end = end.clamp_finite(0, len) as usize;
927
928        // 9. Let searchLength be the length of searchStr.
929        let search_length = search_str.len();
930
931        // 10. If searchLength = 0, return true.
932        if search_length == 0 {
933            return Ok(true.into());
934        }
935
936        // 11. Let start be end - searchLength.
937        if let Some(start) = end.checked_sub(search_length) {
938            // 13. Let substring be the substring of S from start to end.
939            // 14. Return ! SameValueNonNumeric(substring, searchStr).
940            // `SameValueNonNumeric` forwards to `==`, so directly check
941            // equality to avoid converting to `JsValue`
942            Ok(JsValue::new(search_str == string.get_expect(start..end)))
943        } else {
944            // 12. If start < 0, return false.
945            Ok(false.into())
946        }
947    }
948
949    /// `String.prototype.includes( searchString[, position] )`
950    ///
951    /// The `includes()` method determines whether one string may be found within another string, returning `true` or `false` as appropriate.
952    ///
953    /// More information:
954    ///  - [ECMAScript reference][spec]
955    ///  - [MDN documentation][mdn]
956    ///
957    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.includes
958    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes
959    pub(crate) fn includes(
960        this: &JsValue,
961        args: &[JsValue],
962        context: &mut Context,
963    ) -> JsResult<JsValue> {
964        // 1. Let O be ? RequireObjectCoercible(this value).
965        let this = this.require_object_coercible()?;
966
967        // 2. Let S be ? ToString(O).
968        let string = this.to_string(context)?;
969
970        let search_str = match args.get_or_undefined(0) {
971            // 3. Let isRegExp be ? IsRegExp(searchString).
972            search_string if RegExp::is_reg_exp(search_string, context)?.is_some() => {
973                return Err(JsNativeError::typ().with_message(
974                    // 4. If isRegExp is true, throw a TypeError exception.
975                    "First argument to String.prototype.includes must not be a regular expression",
976                ).into());
977            }
978            // 5. Let searchStr be ? ToString(searchString).
979            search_string => search_string.to_string(context)?,
980        };
981
982        // 6. Let pos be ? ToIntegerOrInfinity(position).
983        // 7. Assert: If position is undefined, then pos is 0.
984        let pos = args.get_or_undefined(1).to_integer_or_infinity(context)?;
985
986        // 8. Let len be the length of S.
987        // 9. Let start be the result of clamping pos between 0 and len.
988        let start = pos.clamp_finite(0, string.len() as i64) as usize;
989
990        // 10. Let index be ! StringIndexOf(S, searchStr, start).
991        // 11. If index is not -1, return true.
992        // 12. Return false.
993        Ok(string.index_of(search_str.as_str(), start).is_some().into())
994    }
995
996    /// `String.prototype.replace( regexp|substr, newSubstr|function )`
997    ///
998    /// The `replace()` method returns a new string with some or all matches of a `pattern` replaced by a `replacement`.
999    ///
1000    /// 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.
1001    /// If `pattern` is a string, only the first occurrence will be replaced.
1002    ///
1003    /// The original string is left unchanged.
1004    ///
1005    /// More information:
1006    ///  - [ECMAScript reference][spec]
1007    ///  - [MDN documentation][mdn]
1008    ///
1009    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.replace
1010    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace
1011    pub(crate) fn replace(
1012        this: &JsValue,
1013        args: &[JsValue],
1014        context: &mut Context,
1015    ) -> JsResult<JsValue> {
1016        // Helper enum.
1017        enum CallableOrString {
1018            FunctionalReplace(JsObject),
1019            ReplaceValue(JsString),
1020        }
1021
1022        // 1. Let O be ? RequireObjectCoercible(this value).
1023        let o = this.require_object_coercible()?;
1024
1025        let search_value = args.get_or_undefined(0);
1026        let replace_value = args.get_or_undefined(1);
1027
1028        // 2. If searchValue is an Object, then
1029        if search_value.is_object() {
1030            // a. Let replacer be ? GetMethod(searchValue, @@replace).
1031            let replacer = search_value.get_method(JsSymbol::replace(), context)?;
1032
1033            // b. If replacer is not undefined, then
1034            if let Some(replacer) = replacer {
1035                // i. Return ? Call(replacer, searchValue, « O, replaceValue »).
1036                return replacer.call(search_value, &[o.clone(), replace_value.clone()], context);
1037            }
1038        }
1039
1040        // 3. Let string be ? ToString(O).
1041        let string = o.to_string(context)?;
1042
1043        // 4. Let searchString be ? ToString(searchValue).
1044        let search_string = search_value.to_string(context)?;
1045
1046        // 5. Let functionalReplace be IsCallable(replaceValue).
1047        let functional_replace = replace_value.as_callable();
1048
1049        // 6. If functionalReplace is false, then
1050        let replace_value = if let Some(callable) = functional_replace {
1051            CallableOrString::FunctionalReplace(callable)
1052        } else {
1053            // a. Set replaceValue to ? ToString(replaceValue).
1054            CallableOrString::ReplaceValue(replace_value.to_string(context)?)
1055        };
1056
1057        // 7. Let searchLength be the length of searchString.
1058        let search_length = search_string.len();
1059
1060        // 8. Let position be ! StringIndexOf(string, searchString, 0).
1061        // 9. If position is -1, return string.
1062        let Some(position) = string.index_of(search_string.as_str(), 0) else {
1063            return Ok(string.into());
1064        };
1065
1066        // 10. Let preserved be the substring of string from 0 to position.
1067        let preserved = string.get_expect(..position);
1068
1069        let replacement = match replace_value {
1070            // 11. If functionalReplace is true, then
1071            CallableOrString::FunctionalReplace(replace_fn) => {
1072                // a. Let replacement be ? ToString(? Call(replaceValue, undefined, « searchString, 𝔽(position), string »)).
1073                replace_fn
1074                    .call(
1075                        &JsValue::undefined(),
1076                        &[search_string.into(), position.into(), string.clone().into()],
1077                        context,
1078                    )?
1079                    .to_string(context)?
1080            }
1081            // 12. Else,
1082            CallableOrString::ReplaceValue(replace_value) => {
1083                // a. Assert: Type(replaceValue) is String.
1084                // b. Let captures be a new empty List.
1085                let captures = Vec::new();
1086
1087                // c. Let replacement be ! GetSubstitution(searchString, string, position, captures, undefined, replaceValue).
1088                get_substitution(
1089                    &search_string,
1090                    &string,
1091                    position,
1092                    &captures,
1093                    &JsValue::undefined(),
1094                    &replace_value,
1095                    context,
1096                )?
1097            }
1098        };
1099
1100        // 13. Return the string-concatenation of preserved, replacement, and the substring of string from position + searchLength.
1101        Ok(js_string!(
1102            &preserved,
1103            &replacement,
1104            &string.get_expect(position + search_length..)
1105        )
1106        .into())
1107    }
1108
1109    /// `22.1.3.18 String.prototype.replaceAll ( searchValue, replaceValue )`
1110    ///
1111    /// The `replaceAll()` method returns a new string with all matches of a pattern replaced by a
1112    /// replacement.
1113    ///
1114    /// The pattern can be a string or a `RegExp`, and the replacement can be a string or a
1115    /// function to be called for each match.
1116    ///
1117    /// The original string is left unchanged.
1118    ///
1119    /// More information:
1120    ///  - [ECMAScript reference][spec]
1121    ///  - [MDN documentation][mdn]
1122    ///
1123    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.replaceall
1124    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace
1125    pub(crate) fn replace_all(
1126        this: &JsValue,
1127        args: &[JsValue],
1128        context: &mut Context,
1129    ) -> JsResult<JsValue> {
1130        // 1. Let O be ? RequireObjectCoercible(this value).
1131        let o = this.require_object_coercible()?;
1132
1133        let search_value = args.get_or_undefined(0);
1134        let replace_value = args.get_or_undefined(1);
1135
1136        // 2. If searchValue is an Object, then
1137        if search_value.is_object() {
1138            // a. Let isRegExp be ? IsRegExp(searchValue).
1139            // b. If isRegExp is true, then
1140            if let Some(obj) = RegExp::is_reg_exp(search_value, context)? {
1141                // i. Let flags be ? Get(searchValue, "flags").
1142                let flags = obj.get(js_string!("flags"), context)?;
1143
1144                // ii. Perform ? RequireObjectCoercible(flags).
1145                flags.require_object_coercible()?;
1146
1147                // iii. If ? ToString(flags) does not contain "g", throw a TypeError exception.
1148                if !flags.to_string(context)?.contains(b'g') {
1149                    return Err(JsNativeError::typ()
1150                        .with_message(
1151                            "String.prototype.replaceAll called with a non-global RegExp argument",
1152                        )
1153                        .into());
1154                }
1155            }
1156
1157            // c. Let replacer be ? GetMethod(searchValue, @@replace).
1158            let replacer = search_value.get_method(JsSymbol::replace(), context)?;
1159
1160            // d. If replacer is not undefined, then
1161            if let Some(replacer) = replacer {
1162                // i. Return ? Call(replacer, searchValue, « O, replaceValue »).
1163                return replacer.call(search_value, &[o.clone(), replace_value.clone()], context);
1164            }
1165        }
1166
1167        // 3. Let string be ? ToString(O).
1168        let string = o.to_string(context)?;
1169
1170        // 4. Let searchString be ? ToString(searchValue).
1171        let search_string = search_value.to_string(context)?;
1172
1173        // 5. Let functionalReplace be IsCallable(replaceValue).
1174        let replace = if let Some(f) = replace_value.as_callable() {
1175            Ok(f)
1176        } else {
1177            // 6. If functionalReplace is false, then
1178            // a. Set replaceValue to ? ToString(replaceValue).
1179            Err(replace_value.to_string(context)?)
1180        };
1181
1182        // 7. Let searchLength be the length of searchString.
1183        let search_length = search_string.len();
1184
1185        // 8. Let advanceBy be max(1, searchLength).
1186        let advance_by = max(1, search_length);
1187
1188        // 9. Let matchPositions be a new empty List.
1189        let mut match_positions = Vec::new();
1190
1191        // 10. Let position be ! StringIndexOf(string, searchString, 0).
1192        let mut position = string.index_of(search_string.as_str(), 0);
1193
1194        // 11. Repeat, while position is not -1,
1195        while let Some(p) = position {
1196            // a. Append position to the end of matchPositions.
1197            match_positions.push(p);
1198
1199            // b. Set position to ! StringIndexOf(string, searchString, position + advanceBy).
1200            position = string.index_of(search_string.as_str(), p + advance_by);
1201        }
1202
1203        // 12. Let endOfLastMatch be 0.
1204        let mut end_of_last_match = 0;
1205
1206        // 13. Let result be the empty String.
1207        let mut result = Vec::with_capacity(string.len());
1208
1209        // 14. For each element p of matchPositions, do
1210        for p in match_positions {
1211            // a. Let preserved be the substring of string from endOfLastMatch to p.
1212            let preserved = string.get_expect(end_of_last_match..p);
1213
1214            // c. Else,
1215            let replacement = match replace {
1216                // b. If functionalReplace is true, then
1217                Ok(ref replace_fn) => {
1218                    // i. Let replacement be ? ToString(? Call(replaceValue, undefined, « searchString, 𝔽(p), string »)).
1219                    replace_fn
1220                        .call(
1221                            &JsValue::undefined(),
1222                            &[
1223                                search_string.clone().into(),
1224                                p.into(),
1225                                string.clone().into(),
1226                            ],
1227                            context,
1228                        )?
1229                        .to_string(context)?
1230                }
1231                // i. Assert: Type(replaceValue) is String.
1232                // ii. Let captures be a new empty List.
1233                // iii. Let replacement be ! GetSubstitution(searchString, string, p, captures, undefined, replaceValue).
1234                Err(ref replace_str) => get_substitution(
1235                    &search_string,
1236                    &string,
1237                    p,
1238                    &[],
1239                    &JsValue::undefined(),
1240                    replace_str,
1241                    context,
1242                )
1243                .js_expect("GetSubstitution should never fail here.")?,
1244            };
1245
1246            // d. Set result to the string-concatenation of result, preserved, and replacement.
1247            result.extend(preserved.iter());
1248            result.extend(replacement.iter());
1249
1250            // e. Set endOfLastMatch to p + searchLength.
1251            end_of_last_match = p + search_length;
1252        }
1253
1254        // 15. If endOfLastMatch < the length of string, then
1255        if end_of_last_match < string.len() {
1256            // a. Set result to the string-concatenation of result and the substring of string from endOfLastMatch.
1257            result.extend(string.get_expect(end_of_last_match..).iter());
1258        }
1259
1260        // 16. Return result.
1261        Ok(js_string!(&result[..]).into())
1262    }
1263
1264    /// `String.prototype.indexOf( searchValue[, fromIndex] )`
1265    ///
1266    /// The `indexOf()` method returns the index within the calling `String` object of the first occurrence
1267    /// of the specified value, starting the search at `fromIndex`.
1268    ///
1269    /// Returns `-1` if the value is not found.
1270    ///
1271    /// More information:
1272    ///  - [ECMAScript reference][spec]
1273    ///  - [MDN documentation][mdn]
1274    ///
1275    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.indexof
1276    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/indexOf
1277    pub(crate) fn index_of(
1278        this: &JsValue,
1279        args: &[JsValue],
1280        context: &mut Context,
1281    ) -> JsResult<JsValue> {
1282        // 1. Let O be ? RequireObjectCoercible(this value).
1283        let this = this.require_object_coercible()?;
1284
1285        // 2. Let S be ? ToString(O).
1286        let string = this.to_string(context)?;
1287
1288        // 3. Let searchStr be ? ToString(searchString).
1289        let search_str = args.get_or_undefined(0).to_string(context)?;
1290
1291        // 4. Let pos be ? ToIntegerOrInfinity(position).
1292        // 5. Assert: If position is undefined, then pos is 0.
1293        let pos = args.get_or_undefined(1).to_integer_or_infinity(context)?;
1294
1295        // 6. Let len be the length of S.
1296        let len = string.len() as i64;
1297
1298        // 7. Let start be the result of clamping pos between 0 and len.
1299        let start = pos.clamp_finite(0, len) as usize;
1300
1301        // 8. Return 𝔽(! StringIndexOf(S, searchStr, start)).
1302        Ok(string
1303            .index_of(search_str.as_str(), start)
1304            .map_or(-1, |i| i as i64)
1305            .into())
1306    }
1307
1308    /// `String.prototype.isWellFormed ( )`
1309    ///
1310    /// More information:
1311    ///  - [ECMAScript reference][spec]
1312    ///  - [MDN documentation][mdn]
1313    ///
1314    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.iswellformed
1315    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/isWellFormed
1316    pub(crate) fn is_well_formed(
1317        this: &JsValue,
1318        _: &[JsValue],
1319        context: &mut Context,
1320    ) -> JsResult<JsValue> {
1321        // 1. Let O be ? RequireObjectCoercible(this value).
1322        let o = this.require_object_coercible()?;
1323
1324        // 2. Let S be ? ToString(O).
1325        let s = o.to_string(context)?;
1326
1327        // 3. Return IsStringWellFormedUnicode(S).
1328        for code_point in s.code_points() {
1329            if let CodePoint::UnpairedSurrogate(_) = code_point {
1330                return Ok(false.into());
1331            }
1332        }
1333        Ok(true.into())
1334    }
1335
1336    /// `String.prototype.lastIndexOf( searchValue[, fromIndex] )`
1337    ///
1338    /// The `lastIndexOf()` method returns the index within the calling `String` object of the last occurrence
1339    /// of the specified value, searching backwards from `fromIndex`.
1340    ///
1341    /// Returns `-1` if the value is not found.
1342    ///
1343    /// More information:
1344    ///  - [ECMAScript reference][spec]
1345    ///  - [MDN documentation][mdn]
1346    ///
1347    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.lastindexof
1348    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/lastIndexOf
1349    pub(crate) fn last_index_of(
1350        this: &JsValue,
1351        args: &[JsValue],
1352        context: &mut Context,
1353    ) -> JsResult<JsValue> {
1354        // 1. Let O be ? RequireObjectCoercible(this value).
1355        let this = this.require_object_coercible()?;
1356
1357        // 2. Let S be ? ToString(O).
1358        let string = this.to_string(context)?;
1359
1360        // 3. Let searchStr be ? ToString(searchString).
1361        let search_str = args.get_or_undefined(0).to_string(context)?;
1362
1363        // 4. Let numPos be ? ToNumber(position).
1364        // 5. Assert: If position is undefined, then numPos is NaN.
1365        let num_pos = args.get_or_undefined(1).to_number(context)?;
1366
1367        // 6. If numPos is NaN, let pos be +∞; otherwise, let pos be ! ToIntegerOrInfinity(numPos).
1368        let pos = if num_pos.is_nan() {
1369            IntegerOrInfinity::PositiveInfinity
1370        } else {
1371            JsValue::new(num_pos)
1372                .to_integer_or_infinity(context)
1373                .js_expect("Already called `to_number so this must not fail.")?
1374        };
1375
1376        // 7. Let len be the length of S.
1377        let len = string.len();
1378        // 8. Let start be the result of clamping pos between 0 and len.
1379        let start = pos.clamp_finite(0, len as i64) as usize;
1380
1381        // 9. If searchStr is the empty String, return 𝔽(start).
1382        if search_str.is_empty() {
1383            return Ok(JsValue::new(start));
1384        }
1385
1386        // 10. Let searchLen be the length of searchStr.
1387        let search_len = search_str.len();
1388
1389        if let Some(end) = len.checked_sub(search_len) {
1390            // 11. For each non-negative integer i starting with start such that i ≤ len - searchLen, in descending order, do
1391            for i in (0..=min(start, end)).rev() {
1392                // a. Let candidate be the substring of S from i to i + searchLen.
1393                let candidate = string.get_expect(i..i + search_len);
1394
1395                // b. If candidate is the same sequence of code units as searchStr, return 𝔽(i).
1396                if candidate == search_str {
1397                    return Ok(i.into());
1398                }
1399            }
1400        }
1401
1402        // 12. Return -1𝔽.
1403        Ok(JsValue::new(-1))
1404    }
1405
1406    /// [`String.prototype.localeCompare ( that [ , locales [ , options ] ] )`][spec]
1407    ///
1408    /// Returns a number indicating whether a reference string comes before, or after, or is the
1409    /// same as the given string in sort order.
1410    ///
1411    /// More information:
1412    ///  - [MDN documentation][mdn]
1413    ///
1414    /// [spec]: https://tc39.es/ecma402/#sup-String.prototype.localeCompare
1415    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare
1416    pub(crate) fn locale_compare(
1417        this: &JsValue,
1418        args: &[JsValue],
1419        context: &mut Context,
1420    ) -> JsResult<JsValue> {
1421        // 1. Let O be ? RequireObjectCoercible(this value).
1422        let o = this.require_object_coercible()?;
1423
1424        // 2. Let S be ? ToString(O).
1425        let s = o.to_string(context)?;
1426
1427        // 3. Let thatValue be ? ToString(that).
1428        let that_value = args.get_or_undefined(0).to_string(context)?;
1429
1430        let cmp = {
1431            #[cfg(feature = "intl")]
1432            {
1433                use crate::builtins::intl::Collator;
1434
1435                // 4. Let collator be ? Construct(%Collator%, « locales, options »).
1436                let collator = Collator::constructor(
1437                    &context
1438                        .intrinsics()
1439                        .constructors()
1440                        .collator()
1441                        .constructor()
1442                        .into(),
1443                    args.get(1..).unwrap_or_default(),
1444                    context,
1445                )?;
1446
1447                let object = collator.as_object();
1448                let collator = object
1449                    .as_ref()
1450                    .and_then(|o| o.downcast_ref::<Collator>())
1451                    .js_expect("constructor must return a `Collator` object")?;
1452
1453                let s = s.iter().collect::<Vec<_>>();
1454                let that_value = that_value.iter().collect::<Vec<_>>();
1455
1456                collator
1457                    .collator()
1458                    .as_borrowed()
1459                    .compare_utf16(&s, &that_value) as i8
1460            }
1461
1462            // Default to common comparison if the user doesn't have `Intl` enabled.
1463            #[cfg(not(feature = "intl"))]
1464            {
1465                s.cmp(&that_value) as i8
1466            }
1467        };
1468
1469        // 5. Return CompareStrings(collator, S, thatValue).
1470        Ok(cmp.into())
1471    }
1472
1473    /// `String.prototype.match( regexp )`
1474    ///
1475    /// The `match()` method retrieves the result of matching a **string** against a [`regular expression`][regex].
1476    ///
1477    /// More information:
1478    ///  - [ECMAScript reference][spec]
1479    ///  - [MDN documentation][mdn]
1480    ///
1481    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.match
1482    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match
1483    /// [regex]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
1484    pub(crate) fn r#match(
1485        this: &JsValue,
1486        args: &[JsValue],
1487        context: &mut Context,
1488    ) -> JsResult<JsValue> {
1489        // 1. Let O be ? RequireObjectCoercible(this value).
1490        let o = this.require_object_coercible()?;
1491
1492        // 2. If regexp is an Object, then
1493        let regexp = args.get_or_undefined(0);
1494        if regexp.is_object() {
1495            // a. Let matcher be ? GetMethod(regexp, @@match).
1496            let matcher = regexp.get_method(JsSymbol::r#match(), context)?;
1497            // b. If matcher is not undefined, then
1498            if let Some(matcher) = matcher {
1499                // i. Return ? Call(matcher, regexp, « O »).
1500                return matcher.call(regexp, std::slice::from_ref(o), context);
1501            }
1502        }
1503
1504        // 3. Let S be ? ToString(O).
1505        let s = o.to_string(context)?;
1506
1507        // 4. Let rx be ? RegExpCreate(regexp, undefined).
1508        let rx = RegExp::create(regexp, &JsValue::undefined(), context)?;
1509
1510        // 5. Return ? Invoke(rx, @@match, « S »).
1511        rx.invoke(JsSymbol::r#match(), &[JsValue::new(s)], context)
1512    }
1513
1514    /// Abstract operation `StringPad ( O, maxLength, fillString, placement )`.
1515    ///
1516    /// Performs the actual string padding for padStart/End.
1517    ///
1518    /// More information:
1519    /// - [ECMAScript reference][spec]
1520    ///
1521    /// [spec]: https://tc39.es/ecma262/#sec-stringpad
1522    pub(crate) fn string_pad(
1523        object: &JsValue,
1524        max_length: &JsValue,
1525        fill_string: &JsValue,
1526        placement: Placement,
1527        context: &mut Context,
1528    ) -> JsResult<JsValue> {
1529        // 1. Let S be ? ToString(O).
1530        let string = object.to_string(context)?;
1531
1532        // 2. Let intMaxLength be ℝ(? ToLength(maxLength)).
1533        let int_max_length = max_length.to_length(context)?;
1534
1535        // 3. Let stringLength be the length of S.
1536        let string_length = string.len() as u64;
1537
1538        // 4. If intMaxLength ≤ stringLength, return S.
1539        if int_max_length <= string_length {
1540            return Ok(string.into());
1541        }
1542
1543        // 5. If fillString is undefined, let filler be the String value consisting solely of the code unit 0x0020 (SPACE).
1544        let filler = if fill_string.is_undefined() {
1545            js_string!("\u{0020}")
1546        } else {
1547            // 6. Else, let filler be ? ToString(fillString).
1548            fill_string.to_string(context)?
1549        };
1550
1551        // 7. If filler is the empty String, return S.
1552        if filler.is_empty() {
1553            return Ok(string.into());
1554        }
1555
1556        // 8. Let fillLen be intMaxLength - stringLength.
1557        let fill_len = int_max_length - string_length;
1558
1559        // Check if the resulting string would exceed the maximum string length
1560        if int_max_length > (Self::MAX_STRING_LENGTH as u64) {
1561            return Err(JsNativeError::range()
1562                .with_message(format!(
1563                    "cannot create a string longer than the maximum allowed length ({})",
1564                    Self::MAX_STRING_LENGTH
1565                ))
1566                .into());
1567        }
1568
1569        let filler_len = filler.len() as u64;
1570
1571        // 9. Let truncatedStringFiller be the String value consisting of repeated
1572        // concatenations of filler truncated to length fillLen.
1573        let repetitions = {
1574            let q = fill_len / filler_len;
1575            let r = fill_len % filler_len;
1576            if r == 0 { q } else { q + 1 }
1577        };
1578
1579        let mut truncated_string_filler = Vec::with_capacity(fill_len as usize);
1580        let filler_slice = filler.to_vec();
1581        for _ in 0..repetitions {
1582            let remaining = fill_len as usize - truncated_string_filler.len();
1583            if remaining >= filler_slice.len() {
1584                truncated_string_filler.extend_from_slice(&filler_slice);
1585            } else {
1586                truncated_string_filler.extend_from_slice(&filler_slice[..remaining]);
1587                break;
1588            }
1589        }
1590        let truncated_string_filler = JsString::from(&truncated_string_filler[..]);
1591
1592        // 10. If placement is start, return the string-concatenation of truncatedStringFiller and S.
1593        if placement == Placement::Start {
1594            Ok(js_string!(&truncated_string_filler, &string).into())
1595        } else {
1596            // 11. Else, return the string-concatenation of S and truncatedStringFiller.
1597            Ok(js_string!(&string, &truncated_string_filler).into())
1598        }
1599    }
1600
1601    /// `String.prototype.padEnd( targetLength[, padString] )`
1602    ///
1603    /// The `padEnd()` method pads the current string with a given string (repeated, if needed) so that the resulting string reaches a given length.
1604    ///
1605    /// The padding is applied from the end of the current string.
1606    ///
1607    /// More information:
1608    ///  - [ECMAScript reference][spec]
1609    ///  - [MDN documentation][mdn]
1610    ///
1611    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.padend
1612    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padEnd
1613    pub(crate) fn pad_end(
1614        this: &JsValue,
1615        args: &[JsValue],
1616        context: &mut Context,
1617    ) -> JsResult<JsValue> {
1618        // 1. Let O be ? RequireObjectCoercible(this value).
1619        let this = this.require_object_coercible()?;
1620
1621        let max_length = args.get_or_undefined(0);
1622        let fill_string = args.get_or_undefined(1);
1623
1624        // 2. Return ? StringPad(O, maxLength, fillString, end).
1625        Self::string_pad(this, max_length, fill_string, Placement::End, context)
1626    }
1627
1628    /// `String.prototype.padStart( targetLength [, padString] )`
1629    ///
1630    /// The `padStart()` method pads the current string with another string (multiple times, if needed) until the resulting string reaches the given length.
1631    ///
1632    /// The padding is applied from the start of the current string.
1633    ///
1634    /// More information:
1635    ///  - [ECMAScript reference][spec]
1636    ///  - [MDN documentation][mdn]
1637    ///
1638    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.padstart
1639    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart
1640    pub(crate) fn pad_start(
1641        this: &JsValue,
1642        args: &[JsValue],
1643        context: &mut Context,
1644    ) -> JsResult<JsValue> {
1645        // 1. Let O be ? RequireObjectCoercible(this value).
1646        let this = this.require_object_coercible()?;
1647
1648        let max_length = args.get_or_undefined(0);
1649        let fill_string = args.get_or_undefined(1);
1650
1651        // 2. Return ? StringPad(O, maxLength, fillString, end).
1652        Self::string_pad(this, max_length, fill_string, Placement::Start, context)
1653    }
1654
1655    /// `String.prototype.trim()`
1656    ///
1657    /// The `trim()` method removes whitespace from both ends of a string.
1658    ///
1659    /// Whitespace in this context is all the whitespace characters (space, tab, no-break space, etc.) and all the line terminator characters (LF, CR, etc.).
1660    ///
1661    /// More information:
1662    ///  - [ECMAScript reference][spec]
1663    ///  - [MDN documentation][mdn]
1664    ///
1665    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.trim
1666    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trim
1667    pub(crate) fn trim(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
1668        // 1. Let S be the this value.
1669        // 2. Return ? TrimString(S, start+end).
1670        let object = this.require_object_coercible()?;
1671        let string = object.to_string(context)?;
1672        Ok(js_string!(string.trim()).into())
1673    }
1674
1675    /// `String.prototype.trimStart()`
1676    ///
1677    /// The `trimStart()` method removes whitespace from the beginning of a string.
1678    ///
1679    /// Whitespace in this context is all the whitespace characters (space, tab, no-break space, etc.) and all the line terminator characters (LF, CR, etc.).
1680    ///
1681    /// More information:
1682    ///  - [ECMAScript reference][spec]
1683    ///  - [MDN documentation][mdn]
1684    ///
1685    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.trimstart
1686    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trimStart
1687    pub(crate) fn trim_start(
1688        this: &JsValue,
1689        _: &[JsValue],
1690        context: &mut Context,
1691    ) -> JsResult<JsValue> {
1692        // 1. Let S be the this value.
1693        // 2. Return ? TrimString(S, start).
1694        let object = this.require_object_coercible()?;
1695        let string = object.to_string(context)?;
1696        Ok(js_string!(string.trim_start()).into())
1697    }
1698
1699    /// `String.prototype.trimEnd()`
1700    ///
1701    /// The `trimEnd()` method removes whitespace from the end of a string.
1702    ///
1703    /// Whitespace in this context is all the whitespace characters (space, tab, no-break space, etc.) and all the line terminator characters (LF, CR, etc.).
1704    ///
1705    /// More information:
1706    ///  - [ECMAScript reference][spec]
1707    ///  - [MDN documentation][mdn]
1708    ///
1709    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.trimend
1710    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trimEnd
1711    pub(crate) fn trim_end(
1712        this: &JsValue,
1713        _: &[JsValue],
1714        context: &mut Context,
1715    ) -> JsResult<JsValue> {
1716        // 1. Let S be the this value.
1717        // 2. Return ? TrimString(S, end).
1718        let object = this.require_object_coercible()?;
1719        let string = object.to_string(context)?;
1720        Ok(string.trim_end().into())
1721    }
1722
1723    /// [`String.prototype.toUpperCase()`][upper] and [`String.prototype.toLowerCase()`][lower]
1724    ///
1725    /// The case methods return the calling string value converted to uppercase or lowercase.
1726    ///
1727    /// The value will be **converted** to a string if it isn't one.
1728    ///
1729    /// More information:
1730    ///  - [MDN documentation][mdn]
1731    ///
1732    /// [upper]: https://tc39.es/ecma262/#sec-string.prototype.toUppercase
1733    /// [lower]: https://tc39.es/ecma262/#sec-string.prototype.toLowercase
1734    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toUpperCase
1735    pub(crate) fn to_case<const UPPER: bool>(
1736        this: &JsValue,
1737        _: &[JsValue],
1738        context: &mut Context,
1739    ) -> JsResult<JsValue> {
1740        // 1. Let O be ? RequireObjectCoercible(this value).
1741        let this = this.require_object_coercible()?;
1742
1743        // 2. Let S be ? ToString(O).
1744        let string = this.to_string(context)?;
1745
1746        // 3. Let sText be ! StringToCodePoints(S).
1747        // 4. Let upperText be the result of toUppercase(sText), according to
1748        // the Unicode Default Case Conversion algorithm.
1749        let text = string.map_valid_segments(|s| {
1750            let cow_str = if UPPER {
1751                s.cow_to_uppercase()
1752            } else {
1753                s.cow_to_lowercase()
1754            };
1755            match cow_str {
1756                Cow::Borrowed(_) => s,
1757                Cow::Owned(replaced_str) => replaced_str,
1758            }
1759        });
1760
1761        // 5. Let L be ! CodePointsToString(upperText).
1762        // 6. Return L.
1763        Ok(text.into())
1764    }
1765
1766    /// [`String.prototype.toLocaleLowerCase ( [ locales ] )`][lower] and
1767    /// [`String.prototype.toLocaleUpperCase ( [ locales ] )`][upper]
1768    ///
1769    /// [lower]: https://tc39.es/ecma402/#sup-string.prototype.tolocalelowercase
1770    /// [upper]: https://tc39.es/ecma402/#sup-string.prototype.tolocaleuppercase
1771    pub(crate) fn to_locale_case<const UPPER: bool>(
1772        this: &JsValue,
1773        args: &[JsValue],
1774        context: &mut Context,
1775    ) -> JsResult<JsValue> {
1776        #[cfg(feature = "intl")]
1777        {
1778            use super::intl::locale::{canonicalize_locale_list, default_locale};
1779
1780            // 1. Let O be ? RequireObjectCoercible(this value).
1781            let this = this.require_object_coercible()?;
1782
1783            // 2. Let S be ? ToString(O).
1784            let string = this.to_string(context)?;
1785
1786            // 3. Return ? TransformCase(S, locales, lower).
1787
1788            //  TransformCase ( S, locales, targetCase )
1789            // https://tc39.es/ecma402/#sec-transform-case
1790
1791            // 1. Let requestedLocales be ? CanonicalizeLocaleList(locales).
1792            // 2. If requestedLocales is not an empty List, then
1793            let requested_locale = if let Some(locale) =
1794                canonicalize_locale_list(args.get_or_undefined(0), context)?
1795                    .into_iter()
1796                    .next()
1797            {
1798                // a. Let requestedLocale be requestedLocales[0].
1799                locale
1800            } else {
1801                // 3. Else,
1802                //     a. Let requestedLocale be ! DefaultLocale().
1803                default_locale(context.intl_provider().locale_canonicalizer()?)
1804            };
1805
1806            // 4. Let noExtensionsLocale be the String value that is requestedLocale with any Unicode locale extension sequences (6.2.1) removed.
1807            // 5. Let availableLocales be a List with language tags that includes the languages for which the Unicode
1808            //    Character Database contains language sensitive case mappings. Implementations may add additional
1809            //    language tags if they support case mapping for additional locales.
1810            // 6. Let match be LookupMatchingLocaleByPrefix(availableLocales, noExtensionsLocale).
1811            // 7. If match is not undefined, let locale be match.[[locale]]; else let locale be "und".
1812            // ICU4X already handles locales in the correct way.
1813            let casemapper = context.intl_provider().case_mapper()?;
1814
1815            // 8. Let codePoints be StringToCodePoints(S).
1816            let result = string.map_valid_segments(|segment| {
1817                if UPPER {
1818                    // 10. Else,
1819                    //     a. Assert: targetCase is upper.
1820                    //     b. Let newCodePoints be a List whose elements are the result of an uppercase transformation of codePoints according to an implementation-derived algorithm using locale or the Unicode Default Case Conversion algorithm.
1821                    casemapper
1822                        .as_borrowed()
1823                        .uppercase_to_string(&segment, &requested_locale.id)
1824                        .into()
1825                } else {
1826                    // 9. If targetCase is lower, then
1827                    //     a. Let newCodePoints be a List whose elements are the result of a lowercase transformation of codePoints according to an implementation-derived algorithm using locale or the Unicode Default Case Conversion algorithm.
1828                    casemapper
1829                        .as_borrowed()
1830                        .lowercase_to_string(&segment, &requested_locale.id)
1831                        .into()
1832                }
1833            });
1834
1835            // 11. Return CodePointsToString(newCodePoints).
1836            Ok(result.into())
1837        }
1838
1839        #[cfg(not(feature = "intl"))]
1840        {
1841            Self::to_case::<UPPER>(this, args, context)
1842        }
1843    }
1844
1845    /// `String.prototype.toWellFormed ( )`
1846    ///
1847    /// More information:
1848    ///  - [ECMAScript reference][spec]
1849    ///  - [MDN documentation][mdn]
1850    ///
1851    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.towellformed
1852    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toWellFormed
1853    pub(crate) fn to_well_formed(
1854        this: &JsValue,
1855        _: &[JsValue],
1856        context: &mut Context,
1857    ) -> JsResult<JsValue> {
1858        // 1. Let O be ? RequireObjectCoercible(this value).
1859        let o = this.require_object_coercible()?;
1860
1861        // 2. Let S be ? ToString(O).
1862        let s = o.to_string(context)?;
1863
1864        // 3. Let strLen be the length of S.
1865        // 4. Let k be 0.
1866        // 5. Let result be the empty String.
1867        // 6. Repeat, while k < strLen,
1868        // a. Let cp be CodePointAt(S, k).
1869        // b. If cp.[[IsUnpairedSurrogate]] is true, then
1870        // i. Set result to the string-concatenation of result and 0xFFFD (REPLACEMENT CHARACTER).
1871        // c. Else,
1872        // i. Set result to the string-concatenation of result and UTF16EncodeCodePoint(cp.[[CodePoint]]).
1873        // d. Set k to k + cp.[[CodeUnitCount]].
1874        let result = s
1875            .code_points()
1876            .map(|code_point| match code_point {
1877                CodePoint::UnpairedSurrogate(_) => '\u{FFFD}',
1878                CodePoint::Unicode(char) => char,
1879            })
1880            .collect::<std::string::String>();
1881
1882        // 7. Return result.
1883        Ok(js_string!(result).into())
1884    }
1885
1886    /// `String.prototype.substring( indexStart[, indexEnd] )`
1887    ///
1888    /// The `substring()` method returns the part of the `string` between the start and end indexes, or to the end of the string.
1889    ///
1890    /// More information:
1891    ///  - [ECMAScript reference][spec]
1892    ///  - [MDN documentation][mdn]
1893    ///
1894    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.substring
1895    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/substring
1896    pub(crate) fn substring(
1897        this: &JsValue,
1898        args: &[JsValue],
1899        context: &mut Context,
1900    ) -> JsResult<JsValue> {
1901        // 1. Let O be ? RequireObjectCoercible(this value).
1902        let this = this.require_object_coercible()?;
1903
1904        // 2. Let S be ? ToString(O).
1905        let string = this.to_string(context)?;
1906
1907        // 3. Let len be the length of S.
1908        let len = string.len() as i64;
1909
1910        // 4. Let intStart be ? ToIntegerOrInfinity(start).
1911        let int_start = args.get_or_undefined(0).to_integer_or_infinity(context)?;
1912
1913        // 5. If end is undefined, let intEnd be len; else let intEnd be ? ToIntegerOrInfinity(end).
1914        let int_end = args
1915            .get_or_undefined(1)
1916            .map_or(Ok(IntegerOrInfinity::Integer(len)), |end| {
1917                end.to_integer_or_infinity(context)
1918            })?;
1919
1920        // 6. Let finalStart be the result of clamping intStart between 0 and len.
1921        let final_start = int_start.clamp_finite(0, len) as usize;
1922
1923        // 7. Let finalEnd be the result of clamping intEnd between 0 and len.
1924        let final_end = int_end.clamp_finite(0, len) as usize;
1925
1926        // 8. Let from be min(finalStart, finalEnd).
1927        let from = min(final_start, final_end);
1928
1929        // 9. Let to be max(finalStart, finalEnd).
1930        let to = max(final_start, final_end);
1931
1932        // 10. Return the substring of S from from to to.
1933        // SAFETY: We already checked that `from` and `to` are within bounds.
1934        Ok(unsafe { JsString::slice_unchecked(&string, from, to).into() })
1935    }
1936
1937    /// `String.prototype.split ( separator, limit )`
1938    ///
1939    /// The `split()` method divides a String into an ordered list of substrings, puts these substrings into an array, and returns the array.
1940    /// The division is done by searching for a pattern; where the pattern is provided as the first parameter in the method's call.
1941    ///
1942    /// More information:
1943    ///  - [ECMAScript reference][spec]
1944    ///  - [MDN documentation][mdn]
1945    ///
1946    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.split
1947    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split
1948    pub(crate) fn split(
1949        this: &JsValue,
1950        args: &[JsValue],
1951        context: &mut Context,
1952    ) -> JsResult<JsValue> {
1953        // 1. Let O be ? RequireObjectCoercible(this value).
1954        let this = this.require_object_coercible()?;
1955
1956        let separator = args.get_or_undefined(0);
1957        let limit = args.get_or_undefined(1);
1958
1959        // 2. If separator is an Object, then
1960        if separator.is_object() {
1961            // a. Let splitter be ? GetMethod(separator, @@split).
1962            let splitter = separator.get_method(JsSymbol::split(), context)?;
1963            // b. If splitter is not undefined, then
1964            if let Some(splitter) = splitter {
1965                // i. Return ? Call(splitter, separator, « O, limit »).
1966                return splitter.call(separator, &[this.clone(), limit.clone()], context);
1967            }
1968        }
1969
1970        // 3. Let S be ? ToString(O).
1971        let this_str = this.to_string(context)?;
1972
1973        // 4.  If limit is undefined, let lim be 2^32 - 1; else let lim be ℝ(? ToUint32(limit)).
1974        let lim = if limit.is_undefined() {
1975            u32::MAX
1976        } else {
1977            limit.to_u32(context)?
1978        } as usize;
1979
1980        // 5. Let R be ? ToString(separator).
1981        let separator_str = separator.to_string(context)?;
1982
1983        // 6. If lim = 0, return A.
1984        if lim == 0 {
1985            // a. Return ! CreateArrayFromList(« »).
1986            return Ok(Array::create_array_from_list([], context).into());
1987        }
1988
1989        // 7. If separator is undefined, then
1990        if separator.is_undefined() {
1991            // a. Return ! CreateArrayFromList(« S »).
1992            return Ok(Array::create_array_from_list([this_str.into()], context).into());
1993        }
1994
1995        // 8. Let separatorLength be the length of R.
1996        let separator_length = separator_str.len();
1997
1998        // 9. If separatorLength is 0, then
1999        if separator_length == 0 {
2000            // a. Let head be the substring of S from 0 to lim.
2001            // b. Let codeUnits be a List consisting of the sequence of code units that are the elements of head.
2002            let head_str = this_str.get(..lim).unwrap_or(this_str);
2003            let head = head_str
2004                .iter()
2005                .map(|code| js_string!(std::slice::from_ref(&code)).into());
2006
2007            // c. Return ! CreateArrayFromList(codeUnits).
2008            return Ok(Array::create_array_from_list(head, context).into());
2009        }
2010
2011        // 10. If S is the empty String, return ! CreateArrayFromList(« S »).
2012        if this_str.is_empty() {
2013            return Ok(Array::create_array_from_list([this_str.into()], context).into());
2014        }
2015
2016        // 11. Let substrings be a new empty List.
2017        let mut substrings = vec![];
2018
2019        // 12. Let i be 0.
2020        let mut i = 0;
2021
2022        // 13. Let j be ! StringIndexOf(S, R, 0).
2023        let mut j = this_str.index_of(separator_str.as_str(), 0);
2024
2025        // 14. Repeat, while j is not -1
2026        while let Some(index) = j {
2027            // a. Let T be the substring of S from i to j.
2028            // b. Append T as the last element of substrings.
2029            substrings.push(this_str.slice(i, index));
2030
2031            // c. If the number of elements of substrings is lim, return ! CreateArrayFromList(substrings).
2032            if substrings.len() == lim {
2033                return Ok(Array::create_array_from_list(
2034                    substrings.into_iter().map(JsValue::from),
2035                    context,
2036                )
2037                .into());
2038            }
2039            // d. Set i to j + separatorLength.
2040            i = index + separator_length;
2041
2042            // e. Set j to ! StringIndexOf(S, R, i).
2043            j = this_str.index_of(separator_str.as_str(), i);
2044        }
2045
2046        // 15. Let T be the substring of S from i.
2047        // 16. Append T to substrings.
2048        substrings.push(this_str.slice(i, this_str.len()));
2049
2050        // 17. Return ! CreateArrayFromList(substrings).
2051        Ok(
2052            Array::create_array_from_list(substrings.into_iter().map(JsValue::from), context)
2053                .into(),
2054        )
2055    }
2056
2057    /// `String.prototype.valueOf()`
2058    ///
2059    /// The `valueOf()` method returns the primitive value of a `String` object.
2060    ///
2061    /// More information:
2062    ///  - [ECMAScript reference][spec]
2063    ///  - [MDN documentation][mdn]
2064    ///
2065    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.value_of
2066    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/valueOf
2067    pub(crate) fn value_of(
2068        this: &JsValue,
2069        _args: &[JsValue],
2070        _context: &mut Context,
2071    ) -> JsResult<JsValue> {
2072        // 1. Return ? thisStringValue(this value).
2073        Self::this_string_value(this).map(JsValue::from)
2074    }
2075
2076    /// `String.prototype.matchAll( regexp )`
2077    ///
2078    /// The `matchAll()` method returns an iterator of all results matching a string against a [`regular expression`][regex], including [capturing groups][cg].
2079    ///
2080    /// More information:
2081    ///  - [ECMAScript reference][spec]
2082    ///  - [MDN documentation][mdn]
2083    ///
2084    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.matchall
2085    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/matchAll
2086    /// [regex]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
2087    /// [cg]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Groups_and_Ranges
2088    pub(crate) fn match_all(
2089        this: &JsValue,
2090        args: &[JsValue],
2091        context: &mut Context,
2092    ) -> JsResult<JsValue> {
2093        // 1. Let O be ? RequireObjectCoercible(this value).
2094        let o = this.require_object_coercible()?;
2095
2096        // 2. If regexp is an Object, then
2097        let regexp = args.get_or_undefined(0);
2098        if regexp.is_object() {
2099            // a. Let isRegExp be ? IsRegExp(regexp).
2100            // b. If isRegExp is true, then
2101            if let Some(regexp) = RegExp::is_reg_exp(regexp, context)? {
2102                // i. Let flags be ? Get(regexp, "flags").
2103                let flags = regexp.get(js_string!("flags"), context)?;
2104
2105                // ii. Perform ? RequireObjectCoercible(flags).
2106                flags.require_object_coercible()?;
2107
2108                // iii. If ? ToString(flags) does not contain "g", throw a TypeError exception.
2109                if !flags.to_string(context)?.contains(b'g') {
2110                    return Err(JsNativeError::typ()
2111                        .with_message(
2112                            "String.prototype.matchAll called with a non-global RegExp argument",
2113                        )
2114                        .into());
2115                }
2116            }
2117            // c. Let matcher be ? GetMethod(regexp, @@matchAll).
2118            let matcher = regexp.get_method(JsSymbol::match_all(), context)?;
2119            // d. If matcher is not undefined, then
2120            if let Some(matcher) = matcher {
2121                return matcher.call(regexp, std::slice::from_ref(o), context);
2122            }
2123        }
2124
2125        // 3. Let S be ? ToString(O).
2126        let s = o.to_string(context)?;
2127
2128        // 4. Let rx be ? RegExpCreate(regexp, "g").
2129        let rx = RegExp::create(regexp, &JsValue::new(js_string!("g")), context)?;
2130
2131        // 5. Return ? Invoke(rx, @@matchAll, « S »).
2132        rx.invoke(JsSymbol::match_all(), &[JsValue::new(s)], context)
2133    }
2134
2135    /// `String.prototype.normalize( [ form ] )`
2136    ///
2137    /// The `normalize()` method normalizes a string into a form specified in the Unicode® Standard Annex #15
2138    ///
2139    /// More information:
2140    ///  - [ECMAScript reference][spec]
2141    ///  - [MDN documentation][mdn]
2142    ///
2143    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.normalize
2144    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/normalize
2145    pub(crate) fn normalize(
2146        this: &JsValue,
2147        args: &[JsValue],
2148        context: &mut Context,
2149    ) -> JsResult<JsValue> {
2150        /// Represents the type of normalization applied to a [`JsString`]
2151        #[derive(Clone, Copy)]
2152        pub(crate) enum Normalization {
2153            Nfc,
2154            Nfd,
2155            Nfkc,
2156            Nfkd,
2157        }
2158
2159        // 1. Let O be ? RequireObjectCoercible(this value).
2160        let this = this.require_object_coercible()?;
2161
2162        // 2. Let S be ? ToString(O).
2163        let s = this.to_string(context)?;
2164
2165        // 6. Let ns be the String value that is the result of normalizing S
2166        // into the normalization form named by f as specified in
2167        // https://unicode.org/reports/tr15/.
2168        let first = args.get_or_undefined(0);
2169        // 3. If form is undefined, let f be "NFC".
2170        let normalization = if first.is_undefined() {
2171            Normalization::Nfc
2172        } else {
2173            // 4. Else, let f be ? ToString(form).
2174            match first.to_string(context)? {
2175                ntype if &ntype == "NFC" => Normalization::Nfc,
2176                ntype if &ntype == "NFD" => Normalization::Nfd,
2177                ntype if &ntype == "NFKC" => Normalization::Nfkc,
2178                ntype if &ntype == "NFKD" => Normalization::Nfkd,
2179                // 5. If f is not one of "NFC", "NFD", "NFKC", or "NFKD", throw a RangeError exception.
2180                _ => {
2181                    return Err(JsNativeError::range()
2182                        .with_message(
2183                            "The normalization form should be one of NFC, NFD, NFKC, NFKD.",
2184                        )
2185                        .into());
2186                }
2187            }
2188        };
2189
2190        let normalizers = {
2191            #[cfg(not(feature = "intl"))]
2192            {
2193                const NORMALIZERS: StringNormalizers = StringNormalizers {
2194                    nfc: ComposingNormalizer::new_nfc().static_to_owned(),
2195                    nfkc: ComposingNormalizer::new_nfkc().static_to_owned(),
2196                    nfd: DecomposingNormalizer::new_nfd().static_to_owned(),
2197                    nfkd: DecomposingNormalizer::new_nfkd().static_to_owned(),
2198                };
2199                &NORMALIZERS
2200            }
2201            #[cfg(feature = "intl")]
2202            {
2203                context.intl_provider().string_normalizers()?
2204            }
2205        };
2206
2207        let s = s.iter().collect::<Vec<_>>();
2208
2209        let result = match normalization {
2210            Normalization::Nfc => normalizers.nfc.as_borrowed().normalize_utf16(&s),
2211            Normalization::Nfd => normalizers.nfd.as_borrowed().normalize_utf16(&s),
2212            Normalization::Nfkc => normalizers.nfkc.as_borrowed().normalize_utf16(&s),
2213            Normalization::Nfkd => normalizers.nfkd.as_borrowed().normalize_utf16(&s),
2214        };
2215
2216        // 7. Return ns.
2217        Ok(js_string!(&result[..]).into())
2218    }
2219
2220    /// `String.prototype.search( regexp )`
2221    ///
2222    /// The `search()` method executes a search for a match between a regular expression and this String object.
2223    ///
2224    /// More information:
2225    ///  - [ECMAScript reference][spec]
2226    ///  - [MDN documentation][mdn]
2227    ///
2228    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.search
2229    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/search
2230    pub(crate) fn search(
2231        this: &JsValue,
2232        args: &[JsValue],
2233        context: &mut Context,
2234    ) -> JsResult<JsValue> {
2235        // 1. Let O be ? RequireObjectCoercible(this value).
2236        let o = this.require_object_coercible()?;
2237
2238        // 2. If regexp is an Object, then
2239        let regexp = args.get_or_undefined(0);
2240        if regexp.is_object() {
2241            // a. Let searcher be ? GetMethod(regexp, @@search).
2242            let searcher = regexp.get_method(JsSymbol::search(), context)?;
2243            // b. If searcher is not undefined, then
2244            if let Some(searcher) = searcher {
2245                // i. Return ? Call(searcher, regexp, « O »).
2246                return searcher.call(regexp, std::slice::from_ref(o), context);
2247            }
2248        }
2249
2250        // 3. Let string be ? ToString(O).
2251        let string = o.to_string(context)?;
2252
2253        // 4. Let rx be ? RegExpCreate(regexp, undefined).
2254        let rx = RegExp::create(regexp, &JsValue::undefined(), context)?;
2255
2256        // 5. Return ? Invoke(rx, @@search, « string »).
2257        rx.invoke(JsSymbol::search(), &[JsValue::new(string)], context)
2258    }
2259
2260    pub(crate) fn iterator(
2261        this: &JsValue,
2262        _: &[JsValue],
2263        context: &mut Context,
2264    ) -> JsResult<JsValue> {
2265        // 1. Let O be ? RequireObjectCoercible(this value).
2266        let o = this.require_object_coercible()?;
2267        // 2. Let s be ? ToString(O).
2268        let s = o.to_string(context)?;
2269
2270        Ok(StringIterator::create_string_iterator(s, context).into())
2271    }
2272}
2273
2274#[cfg(feature = "annex-b")]
2275impl String {
2276    /// `String.prototype.substr( start[, length] )`
2277    ///
2278    /// The `substr()` method returns a portion of the string, starting at the specified index and extending for a given number of characters afterward.
2279    ///
2280    /// More information:
2281    ///  - [ECMAScript reference][spec]
2282    ///  - [MDN documentation][mdn]
2283    ///
2284    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.substr
2285    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/substr
2286    /// <https://tc39.es/ecma262/#sec-string.prototype.substr>
2287    pub(crate) fn substr(
2288        this: &JsValue,
2289        args: &[JsValue],
2290        context: &mut Context,
2291    ) -> JsResult<JsValue> {
2292        // 1. Let O be ? RequireObjectCoercible(this value).
2293        let o = this.require_object_coercible()?;
2294
2295        // 2. Let S be ? ToString(O).
2296        let s = o.to_string(context)?;
2297
2298        // 3. Let size be the length of S.
2299        let size = s.len() as i64;
2300
2301        // 4. Let intStart be ? ToIntegerOrInfinity(start).
2302        let start = args.get_or_undefined(0);
2303        let int_start = start.to_integer_or_infinity(context)?;
2304
2305        let int_start = match int_start {
2306            // 5. If intStart is -∞, set intStart to 0.
2307            IntegerOrInfinity::NegativeInfinity => 0,
2308            // 6. Else if intStart < 0, set intStart to max(size + intStart, 0).
2309            IntegerOrInfinity::Integer(int_start) if int_start < 0 => max(size + int_start, 0),
2310            IntegerOrInfinity::Integer(int_start) => int_start,
2311            // 7. Else, set intStart to min(intStart, size).
2312            //
2313            // NOTE: size will always be smaller than +∞
2314            IntegerOrInfinity::PositiveInfinity => size,
2315        } as usize;
2316
2317        // 8. If length is undefined, let intLength be size;
2318        //    otherwise let intLength be ? ToIntegerOrInfinity(length).
2319        let length = args.get_or_undefined(1);
2320        let int_length = if length.is_undefined() {
2321            IntegerOrInfinity::Integer(size)
2322        } else {
2323            length.to_integer_or_infinity(context)?
2324        };
2325
2326        // 9. Set intLength to the result of clamping intLength between 0 and size.
2327        let int_length = match int_length {
2328            IntegerOrInfinity::NegativeInfinity => 0,
2329            IntegerOrInfinity::PositiveInfinity => size,
2330            IntegerOrInfinity::Integer(i) => i.clamp(0, size),
2331        } as usize;
2332
2333        // 10. Let intEnd be min(intStart + intLength, size).
2334        let int_end = min(int_start + int_length, size as usize);
2335
2336        // 11. Return the substring of S from intStart to intEnd.
2337        if let Some(substr) = s.get(int_start..int_end) {
2338            Ok(substr.into())
2339        } else {
2340            Ok(js_string!().into())
2341        }
2342    }
2343
2344    /// `CreateHTML(string, tag, attribute, value)`
2345    ///
2346    /// More information:
2347    ///  - [ECMAScript reference][spec]
2348    ///
2349    /// [spec]: https://tc39.es/ecma262/#sec-createhtml
2350    pub(crate) fn create_html(
2351        string: &JsValue,
2352        tag: JsStr<'_>,
2353        attribute_and_value: Option<(JsStr<'_>, &JsValue)>,
2354        context: &mut Context,
2355    ) -> JsResult<JsValue> {
2356        // 1. Let str be ? RequireObjectCoercible(string).
2357        let str = string.require_object_coercible()?;
2358
2359        // 2. Let S be ? ToString(str).
2360        let s = str.to_string(context)?;
2361
2362        // 3. Let p1 be the string-concatenation of "<" and tag.
2363        let mut p1 = js_string!(js_str!("<"), tag);
2364
2365        // 4. If attribute is not the empty String, then
2366        if let Some((attribute, value)) = attribute_and_value {
2367            // a. Let V be ? ToString(value).
2368            let v = value.to_string(context)?;
2369
2370            // b. Let escapedV be the String value that is the same as V except that each occurrence
2371            //    of the code unit 0x0022 (QUOTATION MARK) in V has been replaced with the six
2372            //    code unit sequence "&quot;".
2373            let mut escaped_v = Vec::with_capacity(v.len());
2374            for c in &v {
2375                if c == 0x0022 {
2376                    escaped_v.extend(utf16!("&quot;"));
2377                    continue;
2378                }
2379                escaped_v.push(c);
2380            }
2381
2382            // c. Set p1 to the string-concatenation of:
2383            //    p1
2384            //    the code unit 0x0020 (SPACE)
2385            //    attribute
2386            //    the code unit 0x003D (EQUALS SIGN)
2387            //    the code unit 0x0022 (QUOTATION MARK)
2388            //    escapedV
2389            //    the code unit 0x0022 (QUOTATION MARK)
2390            p1 = js_string!(
2391                &p1,
2392                js_str!(" "),
2393                attribute,
2394                js_str!("=\""),
2395                &JsString::from(&escaped_v[..]),
2396                js_str!("\"")
2397            );
2398        }
2399
2400        // 5. Let p2 be the string-concatenation of p1 and ">".
2401        // 6. Let p3 be the string-concatenation of p2 and S.
2402        // 7. Let p4 be the string-concatenation of p3, "</", tag, and ">".
2403        let p4 = js_string!(&p1, js_str!(">"), &s, js_str!("</"), tag, js_str!(">"));
2404
2405        // 8. Return p4.
2406        Ok(p4.into())
2407    }
2408
2409    /// `String.prototype.anchor( name )`
2410    ///
2411    /// More information:
2412    ///  - [ECMAScript reference][spec]
2413    ///  - [MDN documentation][mdn]
2414    ///
2415    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.anchor
2416    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/anchor
2417    pub(crate) fn anchor(
2418        this: &JsValue,
2419        args: &[JsValue],
2420        context: &mut Context,
2421    ) -> JsResult<JsValue> {
2422        let name = args.get_or_undefined(0);
2423
2424        // 1. Let S be the this value.
2425        let s = this;
2426        // 2. Return ? CreateHTML(S, "a", "name", name).
2427        Self::create_html(s, js_str!("a"), Some((js_str!("name"), name)), context)
2428    }
2429
2430    /// `String.prototype.big( )`
2431    ///
2432    /// More information:
2433    ///  - [ECMAScript reference][spec]
2434    ///  - [MDN documentation][mdn]
2435    ///
2436    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.big
2437    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/big
2438    pub(crate) fn big(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2439        // 1. Let S be the this value.
2440        let s = this;
2441        // 2. Return ? CreateHTML(S, "big", "", "").
2442        Self::create_html(s, js_str!("big"), None, context)
2443    }
2444
2445    /// `String.prototype.blink( )`
2446    ///
2447    /// More information:
2448    ///  - [ECMAScript reference][spec]
2449    ///  - [MDN documentation][mdn]
2450    ///
2451    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.blink
2452    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/blink
2453    pub(crate) fn blink(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2454        // 1. Let S be the this value.
2455        let s = this;
2456        // 2. Return ? CreateHTML(S, "blink", "", "").
2457        Self::create_html(s, js_str!("blink"), None, context)
2458    }
2459
2460    /// `String.prototype.bold( )`
2461    ///
2462    /// More information:
2463    ///  - [ECMAScript reference][spec]
2464    ///  - [MDN documentation][mdn]
2465    ///
2466    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.bold
2467    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/bold
2468    pub(crate) fn bold(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2469        // 1. Let S be the this value.
2470        let s = this;
2471        // 2. Return ? CreateHTML(S, "b", "", "").
2472        Self::create_html(s, js_str!("b"), None, context)
2473    }
2474
2475    /// `String.prototype.fixed( )`
2476    ///
2477    /// More information:
2478    ///  - [ECMAScript reference][spec]
2479    ///  - [MDN documentation][mdn]
2480    ///
2481    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.fixed
2482    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/fixed
2483    pub(crate) fn fixed(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2484        // 1. Let S be the this value.
2485        let s = this;
2486        // 2. Return ? CreateHTML(S, "big", "", "").
2487        Self::create_html(s, js_str!("tt"), None, context)
2488    }
2489
2490    /// `String.prototype.fontcolor( color )`
2491    ///
2492    /// More information:
2493    ///  - [ECMAScript reference][spec]
2494    ///  - [MDN documentation][mdn]
2495    ///
2496    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.fontcolor
2497    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/fontcolor
2498    pub(crate) fn fontcolor(
2499        this: &JsValue,
2500        args: &[JsValue],
2501        context: &mut Context,
2502    ) -> JsResult<JsValue> {
2503        let color = args.get_or_undefined(0);
2504
2505        // 1. Let S be the this value.
2506        let s = this;
2507        // 2. Return ? CreateHTML(S, "font", "color", color).
2508        Self::create_html(s, js_str!("font"), Some((js_str!("color"), color)), context)
2509    }
2510
2511    /// `String.prototype.fontsize( size )`
2512    ///
2513    /// More information:
2514    ///  - [ECMAScript reference][spec]
2515    ///  - [MDN documentation][mdn]
2516    ///
2517    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.fontsize
2518    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/fontsize
2519    pub(crate) fn fontsize(
2520        this: &JsValue,
2521        args: &[JsValue],
2522        context: &mut Context,
2523    ) -> JsResult<JsValue> {
2524        let size = args.get_or_undefined(0);
2525
2526        // 1. Let S be the this value.
2527        let s = this;
2528        // 2. Return ? CreateHTML(S, "font", "size", size).
2529        Self::create_html(s, js_str!("font"), Some((js_str!("size"), size)), context)
2530    }
2531
2532    /// `String.prototype.italics( )`
2533    ///
2534    /// More information:
2535    ///  - [ECMAScript reference][spec]
2536    ///  - [MDN documentation][mdn]
2537    ///
2538    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.italics
2539    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/italics
2540    pub(crate) fn italics(
2541        this: &JsValue,
2542        _: &[JsValue],
2543        context: &mut Context,
2544    ) -> JsResult<JsValue> {
2545        // 1. Let S be the this value.
2546        let s = this;
2547        // 2. Return ? CreateHTML(S, "i", "", "").
2548        Self::create_html(s, js_str!("i"), None, context)
2549    }
2550
2551    /// `String.prototype.link( url )`
2552    ///
2553    /// More information:
2554    ///  - [ECMAScript reference][spec]
2555    ///  - [MDN documentation][mdn]
2556    ///
2557    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.link
2558    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/link
2559    pub(crate) fn link(
2560        this: &JsValue,
2561        args: &[JsValue],
2562        context: &mut Context,
2563    ) -> JsResult<JsValue> {
2564        let url = args.get_or_undefined(0);
2565
2566        // 1. Let S be the this value.
2567        let s = this;
2568        // 2. Return ? CreateHTML(S, "a", "href", url).
2569        Self::create_html(s, js_str!("a"), Some((js_str!("href"), url)), context)
2570    }
2571
2572    /// `String.prototype.small( )`
2573    ///
2574    /// More information:
2575    ///  - [ECMAScript reference][spec]
2576    ///  - [MDN documentation][mdn]
2577    ///
2578    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.small
2579    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/small
2580    pub(crate) fn small(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2581        // 1. Let S be the this value.
2582        let s = this;
2583        // 2. Return ? CreateHTML(S, "small", "", "").
2584        Self::create_html(s, js_str!("small"), None, context)
2585    }
2586
2587    /// `String.prototype.strike( )`
2588    ///
2589    /// More information:
2590    ///  - [ECMAScript reference][spec]
2591    ///  - [MDN documentation][mdn]
2592    ///
2593    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.strike
2594    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/strike
2595    pub(crate) fn strike(
2596        this: &JsValue,
2597        _: &[JsValue],
2598        context: &mut Context,
2599    ) -> JsResult<JsValue> {
2600        // 1. Let S be the this value.
2601        let s = this;
2602        // 2. Return ? CreateHTML(S, "strike", "", "").
2603        Self::create_html(s, js_str!("strike"), None, context)
2604    }
2605
2606    /// `String.prototype.sub( )`
2607    ///
2608    /// More information:
2609    ///  - [ECMAScript reference][spec]
2610    ///  - [MDN documentation][mdn]
2611    ///
2612    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.sub
2613    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/sub
2614    pub(crate) fn sub(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2615        // 1. Let S be the this value.
2616        let s = this;
2617        // 2. Return ? CreateHTML(S, "sub", "", "").
2618        Self::create_html(s, js_str!("sub"), None, context)
2619    }
2620
2621    /// `String.prototype.sup( )`
2622    ///
2623    /// More information:
2624    ///  - [ECMAScript reference][spec]
2625    ///  - [MDN documentation][mdn]
2626    ///
2627    /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.sup
2628    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/sup
2629    pub(crate) fn sup(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2630        // 1. Let S be the this value.
2631        let s = this;
2632        // 2. Return ? CreateHTML(S, "sup", "", "").
2633        Self::create_html(s, js_str!("sup"), None, context)
2634    }
2635}
2636
2637/// Abstract operation `GetSubstitution ( matched, str, position, captures, namedCaptures, replacement )`
2638///
2639/// More information:
2640///  - [ECMAScript reference][spec]
2641///
2642/// [spec]: https://tc39.es/ecma262/#sec-getsubstitution
2643pub(crate) fn get_substitution(
2644    matched: &JsString,
2645    str: &JsString,
2646    position: usize,
2647    captures: &[JsValue],
2648    named_captures: &JsValue,
2649    replacement: &JsString,
2650    context: &mut Context,
2651) -> JsResult<JsString> {
2652    let mut buf = [0; 2];
2653    // 1. Assert: Type(matched) is String.
2654
2655    // 2. Let matchLength be the number of code units in matched.
2656    let match_length = matched.len();
2657
2658    // 3. Assert: Type(str) is String.
2659
2660    // 4. Let stringLength be the number of code units in str.
2661    let str_length = str.len();
2662
2663    // 5. Assert: position ≤ stringLength.
2664    // 6. Assert: captures is a possibly empty List of Strings.
2665    // 7. Assert: Type(replacement) is String.
2666
2667    // 8. Let tailPos be position + matchLength.
2668    let tail_pos = position + match_length;
2669
2670    // 10. Let result be the String value derived from replacement by copying code unit elements
2671    //     from replacement to result while performing replacements as specified in Table 58.
2672    //     These $ replacements are done left-to-right, and, once such a replacement is performed,
2673    //     the new replacement text is not subject to further replacements.
2674    let mut result = vec![];
2675    let mut chars = replacement.code_points().peekable();
2676
2677    while let Some(first) = chars.next() {
2678        if first == CodePoint::Unicode('$') {
2679            let second = chars.next();
2680            let second_is_digit = second
2681                .and_then(CodePoint::as_char)
2682                .as_ref()
2683                .is_some_and(char::is_ascii_digit);
2684
2685            match second {
2686                // $$
2687                Some(CodePoint::Unicode('$')) => {
2688                    // $
2689                    result.push('$' as u16);
2690                }
2691                // $&
2692                Some(CodePoint::Unicode('&')) => {
2693                    // matched
2694                    result.extend(matched.iter());
2695                }
2696                // $`
2697                Some(CodePoint::Unicode('`')) => {
2698                    // The replacement is the substring of str from 0 to position.
2699                    result.extend(str.get_expect(..position).iter());
2700                }
2701                // $'
2702                Some(CodePoint::Unicode('\'')) => {
2703                    // If tailPos ≥ stringLength, the replacement is the empty String.
2704                    // Otherwise the replacement is the substring of str from tailPos.
2705                    if tail_pos < str_length {
2706                        result.extend(str.get_expect(tail_pos..).iter());
2707                    }
2708                }
2709                // $nn
2710                // f. Else if templateRemainder starts with "$" followed by 1 or more decimal digits, then
2711                Some(CodePoint::Unicode(second)) if second_is_digit => {
2712                    // i. If templateRemainder starts with "$" followed by 2 or more decimal digits, let digitCount be 2. Otherwise, let digitCount be 1.
2713                    // ii. Let ref be the substring of templateRemainder from 0 to 1 + digitCount.
2714                    // iii. Let digits be the substring of templateRemainder from 1 to 1 + digitCount.
2715                    // iv. Let index be ℝ(StringToNumber(digits)).
2716                    let mut index = second
2717                        .to_digit(10)
2718                        .expect("could not convert character to digit after checking it")
2719                        as usize;
2720
2721                    // vi. Let captureLen be the number of elements in captures.
2722                    let capture_len = captures.len();
2723
2724                    // NOTE(HalidOdat): We deviate from the spec, because of a bug in GetSubstitutions
2725                    //
2726                    // See: https://github.com/tc39/ecma262/issues/1426
2727                    if let Some(digit) = chars
2728                        .peek()
2729                        .copied()
2730                        .and_then(CodePoint::as_char)
2731                        .and_then(|n| n.to_digit(10))
2732                    {
2733                        // If there is two digits, and it's not in range fallback to one digit.
2734                        let two_digit_index = index * 10 + digit as usize;
2735                        if (1..=capture_len).contains(&two_digit_index) {
2736                            index = two_digit_index;
2737                            chars.next();
2738                        }
2739                    }
2740
2741                    // v. Assert: 0 ≤ index ≤ 99.
2742                    debug_assert!((0..=99).contains(&index));
2743
2744                    // vii. If 1 ≤ index ≤ captureLen, then
2745                    if (1..=capture_len).contains(&index) {
2746                        // 1. Let capture be captures[index - 1].
2747                        // 2. If capture is undefined, then
2748                        //     a. Let refReplacement be the empty String.
2749                        // 3. Else,
2750                        //     a. Let refReplacement be capture.
2751                        if let Some(capture) = captures.get(index - 1)
2752                            && let Some(s) = capture.as_string()
2753                        {
2754                            result.extend(s.iter());
2755                        }
2756
2757                    // viii. Else,
2758                    } else {
2759                        // 1. Let refReplacement be ref.
2760                        result.extend_from_slice(&['$' as u16, second as u16]);
2761                    }
2762                }
2763                // $<
2764                Some(CodePoint::Unicode('<')) => {
2765                    // 1. If namedCaptures is undefined, the replacement text is the String "$<".
2766                    // 2. Else,
2767                    if named_captures.is_undefined() {
2768                        result.extend_from_slice(utf16!("$<"));
2769                    } else {
2770                        // a. Assert: Type(namedCaptures) is Object.
2771                        let named_captures = named_captures
2772                            .as_object()
2773                            .js_expect("should be an object according to spec")?;
2774
2775                        // b. Scan until the next > U+003E (GREATER-THAN SIGN).
2776                        let mut group_name = vec![];
2777                        let mut found = false;
2778                        loop {
2779                            match chars.next() {
2780                                Some(CodePoint::Unicode('>')) => {
2781                                    found = true;
2782                                    break;
2783                                }
2784                                Some(c) => group_name.extend_from_slice(c.encode_utf16(&mut buf)),
2785                                None => break,
2786                            }
2787                        }
2788
2789                        // c. If none is found, the replacement text is the String "$<".
2790                        #[allow(clippy::if_not_else)]
2791                        if !found {
2792                            result.extend_from_slice(utf16!("$<"));
2793                            result.extend_from_slice(&group_name);
2794                        // d. Else,
2795                        } else {
2796                            // i. Let groupName be the enclosed substring.
2797                            let group_name = js_string!(&group_name[..]);
2798                            // ii. Let capture be ? Get(namedCaptures, groupName).
2799                            let capture = named_captures.get(group_name, context)?;
2800
2801                            // iii. If capture is undefined, replace the text through > with the empty String.
2802                            // iv. Otherwise, replace the text through > with ? ToString(capture).
2803                            if !capture.is_undefined() {
2804                                result.extend(capture.to_string(context)?.iter());
2805                            }
2806                        }
2807                    }
2808                }
2809                // $?, ? is none of the above
2810                _ => {
2811                    result.push('$' as u16);
2812                    if let Some(second) = second {
2813                        result.extend_from_slice(second.encode_utf16(&mut buf));
2814                    }
2815                }
2816            }
2817        } else {
2818            result.extend_from_slice(first.encode_utf16(&mut buf));
2819        }
2820    }
2821
2822    // 11. Return result.
2823    Ok(js_string!(&result[..]))
2824}