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 """.
2373 let mut escaped_v = Vec::with_capacity(v.len());
2374 for c in &v {
2375 if c == 0x0022 {
2376 escaped_v.extend(utf16!("""));
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}