diff --git a/boa/src/builtins/regexp/mod.rs b/boa/src/builtins/regexp/mod.rs index 5f8f31bc43..4d362779db 100644 --- a/boa/src/builtins/regexp/mod.rs +++ b/boa/src/builtins/regexp/mod.rs @@ -1598,7 +1598,11 @@ impl RegExp { let splitter = constructor .as_object() .expect("SpeciesConstructor returned non Object") - .construct(&[JsValue::from(rx), new_flags.into()], &constructor, context)?; + .construct( + &[JsValue::from(rx), new_flags.into()], + &constructor, + context, + )?; // 11. Let A be ! ArrayCreate(0). let a = Array::array_create(0, None, context).unwrap(); diff --git a/boa/src/builtins/string/mod.rs b/boa/src/builtins/string/mod.rs index 04c3e22927..2525ebc451 100644 --- a/boa/src/builtins/string/mod.rs +++ b/boa/src/builtins/string/mod.rs @@ -131,6 +131,7 @@ impl BuiltIn for String { .method(Self::value_of, "valueOf", 0) .method(Self::match_all, "matchAll", 1) .method(Self::replace, "replace", 2) + .method(Self::replace_all, "replaceAll", 2) .method(Self::iterator, (symbol_iterator, "[Symbol.iterator]"), 0) .method(Self::search, "search", 1) .method(Self::at, "at", 1) @@ -744,15 +745,17 @@ impl String { let search_length = search_str.len(); // 8. Let position be ! StringIndexOf(string, searchString, 0). - let position = this_str.find(search_str.as_str()); - // 9. If position is -1, return string. - if position.is_none() { + let position = if let Some(p) = this_str.index_of(&search_str, 0) { + p + } else { return Ok(this_str.into()); - } + }; // 10. Let preserved be the substring of string from 0 to position. - let preserved = this_str.get(..position.unwrap()); + let preserved = StdString::from_utf16_lossy( + &this_str.encode_utf16().take(position).collect::>(), + ); // 11. If functionalReplace is true, then // 12. Else, @@ -762,11 +765,7 @@ impl String { .call( &replace_value, &JsValue::undefined(), - &[ - search_str.into(), - position.unwrap().into(), - this_str.clone().into(), - ], + &[search_str.into(), position.into(), this_str.clone().into()], )? .to_string(context)? } else { @@ -778,7 +777,7 @@ impl String { get_substitution( search_str.to_string(), this_str.to_string(), - position.unwrap(), + position, captures, JsValue::undefined(), replace_value.to_string(context)?, @@ -789,15 +788,189 @@ impl String { // 13. Return the string-concatenation of preserved, replacement, and the substring of string from position + searchLength. Ok(format!( "{}{}{}", - preserved.unwrap_or_default(), + preserved, replacement, - this_str - .get((position.unwrap() + search_length)..) - .unwrap_or_default() + StdString::from_utf16_lossy( + &this_str + .encode_utf16() + .skip(position + search_length) + .collect::>() + ) ) .into()) } + /// `22.1.3.18 String.prototype.replaceAll ( searchValue, replaceValue )` + /// + /// The replaceAll() method returns a new string with all matches of a pattern replaced by a replacement. + /// + /// 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. + /// + /// The original string is left unchanged. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.replaceall + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace + pub(crate) fn replace_all( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> Result { + // 1. Let O be ? RequireObjectCoercible(this value). + let o = this.require_object_coercible(context)?; + + let search_value = args.get(0).cloned().unwrap_or_default(); + let replace_value = args.get(1).cloned().unwrap_or_default(); + + // 2. If searchValue is neither undefined nor null, then + if !search_value.is_null_or_undefined() { + // a. Let isRegExp be ? IsRegExp(searchValue). + if let Some(obj) = search_value.as_object() { + // b. If isRegExp is true, then + if obj.is_regexp() { + // i. Let flags be ? Get(searchValue, "flags"). + let flags = obj.get("flags", context)?; + + // ii. Perform ? RequireObjectCoercible(flags). + flags.require_object_coercible(context)?; + + // iii. If ? ToString(flags) does not contain "g", throw a TypeError exception. + if !flags.to_string(context)?.contains('g') { + return context.throw_type_error( + "String.prototype.replaceAll called with a non-global RegExp argument", + ); + } + } + } + + // c. Let replacer be ? GetMethod(searchValue, @@replace). + let replacer = search_value + .as_object() + .unwrap_or_default() + .get_method(context, WellKnownSymbols::replace())?; + + // d. If replacer is not undefined, then + if let Some(replacer) = replacer { + // i. Return ? Call(replacer, searchValue, « O, replaceValue »). + return replacer.call(&search_value, &[o.into(), replace_value], context); + } + } + + // 3. Let string be ? ToString(O). + let string = o.to_string(context)?; + + // 4. Let searchString be ? ToString(searchValue). + let search_string = search_value.to_string(context)?; + + // 5. Let functionalReplace be IsCallable(replaceValue). + let functional_replace = replace_value.is_function(); + + // 6. If functionalReplace is false, then + let replace_value_string = if !functional_replace { + // a. Set replaceValue to ? ToString(replaceValue). + replace_value.to_string(context)? + } else { + JsString::new("") + }; + + // 7. Let searchLength be the length of searchString. + let search_length = search_string.encode_utf16().count(); + + // 8. Let advanceBy be max(1, searchLength). + let advance_by = max(1, search_length); + + // 9. Let matchPositions be a new empty List. + let mut match_positions = Vec::new(); + + // 10. Let position be ! StringIndexOf(string, searchString, 0). + let mut position = string.index_of(&search_string, 0); + + // 11. Repeat, while position is not -1, + while let Some(p) = position { + // a. Append position to the end of matchPositions. + match_positions.push(p); + + // b. Set position to ! StringIndexOf(string, searchString, position + advanceBy). + position = string.index_of(&search_string, p + advance_by); + } + + // 12. Let endOfLastMatch be 0. + let mut end_of_last_match = 0; + + // 13. Let result be the empty String. + let mut result = JsString::new(""); + + // 14. For each element p of matchPositions, do + for p in match_positions { + // a. Let preserved be the substring of string from endOfLastMatch to p. + let preserved = StdString::from_utf16_lossy( + &string + .clone() + .encode_utf16() + .skip(end_of_last_match) + .take(p - end_of_last_match) + .collect::>(), + ); + + // b. If functionalReplace is true, then + // c. Else, + let replacement = if functional_replace { + // i. Let replacement be ? ToString(? Call(replaceValue, undefined, « searchString, 𝔽(p), string »)). + context + .call( + &replace_value, + &JsValue::undefined(), + &[ + search_string.clone().into(), + p.into(), + string.clone().into(), + ], + )? + .to_string(context)? + } else { + // i. Assert: Type(replaceValue) is String. + // ii. Let captures be a new empty List. + // iii. Let replacement be ! GetSubstitution(searchString, string, p, captures, undefined, replaceValue). + get_substitution( + search_string.to_string(), + string.to_string(), + p, + Vec::new(), + JsValue::undefined(), + replace_value_string.clone(), + context, + ) + .expect("GetSubstitution should never fail here.") + }; + // d. Set result to the string-concatenation of result, preserved, and replacement. + result = JsString::new(format!("{}{}{}", result.as_str(), &preserved, &replacement)); + + // e. Set endOfLastMatch to p + searchLength. + end_of_last_match = p + search_length; + } + + // 15. If endOfLastMatch < the length of string, then + if end_of_last_match < string.encode_utf16().count() { + // a. Set result to the string-concatenation of result and the substring of string from endOfLastMatch. + result = JsString::new(format!( + "{}{}", + result.as_str(), + &StdString::from_utf16_lossy( + &string + .encode_utf16() + .skip(end_of_last_match) + .collect::>() + ) + )); + } + + // 16. Return result. + Ok(result.into()) + } + /// `String.prototype.indexOf( searchValue[, fromIndex] )` /// /// The `indexOf()` method returns the index within the calling `String` object of the first occurrence @@ -1619,12 +1792,12 @@ pub(crate) fn get_substitution( // 1. Assert: Type(matched) is String. // 2. Let matchLength be the number of code units in matched. - let match_length = matched.chars().count(); + let match_length = matched.encode_utf16().count(); // 3. Assert: Type(str) is String. // 4. Let stringLength be the number of code units in str. - let str_length = str.chars().count(); + let str_length = str.encode_utf16().count(); // 5. Assert: position ≤ stringLength. // 6. Assert: captures is a possibly empty List of Strings. @@ -1665,16 +1838,18 @@ pub(crate) fn get_substitution( // $` (Some('`'), _) => { // The replacement is the substring of str from 0 to position. - result.push_str(&str[..position]); + result.push_str(&StdString::from_utf16_lossy( + &str.encode_utf16().take(position).collect::>(), + )); } // $' (Some('\''), _) => { // If tailPos ≥ stringLength, the replacement is the empty String. // Otherwise the replacement is the substring of str from tailPos. - if tail_pos >= str_length { - result.push_str(""); - } else { - result.push_str(&str[tail_pos..]); + if tail_pos < str_length { + result.push_str(&StdString::from_utf16_lossy( + &str.encode_utf16().skip(tail_pos).collect::>(), + )); } } // $nn diff --git a/boa/src/string.rs b/boa/src/string.rs index b52bc0f0ad..70404a7954 100644 --- a/boa/src/string.rs +++ b/boa/src/string.rs @@ -186,6 +186,56 @@ impl JsString { pub fn ptr_eq(x: &Self, y: &Self) -> bool { x.inner == y.inner } + + /// `6.1.4.1 StringIndexOf ( string, searchValue, fromIndex )` + /// + /// Note: Instead of returning an isize with `-1` as the "not found" value, + /// We make use of the type system and return Option with None as the "not found" value. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-stringindexof + pub(crate) fn index_of(&self, search_value: &Self, from_index: usize) -> Option { + // 1. Assert: Type(string) is String. + // 2. Assert: Type(searchValue) is String. + // 3. Assert: fromIndex is a non-negative integer. + + // 4. Let len be the length of string. + let len = self.encode_utf16().count(); + + // 5. If searchValue is the empty String and fromIndex ≤ len, return fromIndex. + if search_value.is_empty() && from_index <= len { + return Some(from_index); + } + + // 6. Let searchLen be the length of searchValue. + let search_len = search_value.encode_utf16().count(); + + // 7. For each integer i starting with fromIndex such that i ≤ len - searchLen, in ascending order, do + for i in from_index..=len { + if i as isize > (len as isize - search_len as isize) { + break; + } + + // a. Let candidate be the substring of string from i to i + searchLen. + let candidate = String::from_utf16_lossy( + &self + .encode_utf16() + .skip(i) + .take(search_len) + .collect::>(), + ); + + // b. If candidate is the same sequence of code units as searchValue, return i. + if candidate == search_value.as_str() { + return Some(i); + } + } + + // 8. Return -1. + None + } } impl Finalize for JsString {}