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