Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement String.prototype.replaceAll #1469

Merged
merged 3 commits into from
Aug 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion boa/src/builtins/regexp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
219 changes: 197 additions & 22 deletions boa/src/builtins/string/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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::<Vec<u16>>(),
);

// 11. If functionalReplace is true, then
// 12. Else,
Expand All @@ -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 {
Expand All @@ -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)?,
Expand All @@ -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::<Vec<u16>>()
)
)
.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<JsValue> {
// 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::<Vec<u16>>(),
);

// 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::<Vec<u16>>()
)
));
}

// 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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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::<Vec<u16>>(),
));
}
// $'
(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::<Vec<u16>>(),
));
}
}
// $nn
Expand Down
50 changes: 50 additions & 0 deletions boa/src/string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<usize> 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<usize> {
// 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::<Vec<u16>>(),
);

// 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 {}
Expand Down