diff --git a/boa_engine/src/builtins/regexp/mod.rs b/boa_engine/src/builtins/regexp/mod.rs index b11bb67e0f5..0a1caf7470c 100644 --- a/boa_engine/src/builtins/regexp/mod.rs +++ b/boa_engine/src/builtins/regexp/mod.rs @@ -231,7 +231,7 @@ impl RegExp { /// - [ECMAScript reference][spec] /// /// [spec]: https://tc39.es/ecma262/#sec-regexpalloc - fn alloc(new_target: &JsValue, context: &mut Context) -> JsResult { + pub(crate) fn alloc(new_target: &JsValue, context: &mut Context) -> JsResult { // 1. Let obj be ? OrdinaryCreateFromConstructor(newTarget, "%RegExp.prototype%", « [[RegExpMatcher]], [[OriginalSource]], [[OriginalFlags]] »). let proto = get_prototype_from_constructor(new_target, StandardConstructors::regexp, context)?; @@ -259,7 +259,7 @@ impl RegExp { /// - [ECMAScript reference][spec] /// /// [spec]: https://tc39.es/ecma262/#sec-regexpinitialize - fn initialize( + pub(crate) fn initialize( obj: JsObject, pattern: &JsValue, flags: &JsValue, diff --git a/boa_engine/src/object/builtins/jsregexp.rs b/boa_engine/src/object/builtins/jsregexp.rs new file mode 100644 index 00000000000..a76ddb75620 --- /dev/null +++ b/boa_engine/src/object/builtins/jsregexp.rs @@ -0,0 +1,269 @@ +//! This module implements a wrapper for the `RegExp` Builtin Javascript Object +use crate::{ + builtins::RegExp, + object::{JsArray, JsObject, JsObjectType}, + Context, JsResult, JsValue, +}; + +use boa_gc::{Finalize, Trace}; +use std::ops::Deref; + +/// `JsRegExp` provides a wrapper for Boa's implementation of the JavaScript `RegExp` builtin object +/// +/// # Examples +/// +/// Create a `JsRegExp` and run RegExp.prototype.test( String ) +/// +/// ``` +/// # use boa_engine::{ +/// # object::builtins::JsRegExp, +/// # Context, JsValue, +/// # }; +/// +/// // Initialize the `Context` +/// let context = &mut Context::default(); +/// +/// // Create a new RegExp with pattern and flags +/// let regexp = JsRegExp::new("foo", "gi", context).unwrap(); +/// +/// let test_result = regexp.test("football", context).unwrap(); +/// assert!(test_result); +/// +/// let to_string = regexp.to_string(context).unwrap(); +/// assert_eq!(to_string, String::from("/foo/gi")); +/// +/// ``` +/// +#[derive(Debug, Clone, Trace, Finalize)] +pub struct JsRegExp { + inner: JsObject, +} + +impl JsRegExp { + /// Create a new `JsRegExp` object + /// ``` + /// # use boa_engine::{ + /// # object::builtins::JsRegExp, + /// # Context, JsValue, + /// # }; + /// // Initialize the `Context` + /// let context = &mut Context::default(); + /// + /// // Create a new RegExp with pattern and flags + /// let regexp = JsRegExp::new("foo", "gi", context).unwrap(); + /// ``` + #[inline] + pub fn new(pattern: S, flags: S, context: &mut Context) -> JsResult + where + S: Into, + { + let constructor = &context + .intrinsics() + .constructors() + .regexp() + .constructor() + .into(); + let obj = RegExp::alloc(constructor, context)?; + + let regexp = RegExp::initialize(obj, &pattern.into(), &flags.into(), context)? + .as_object() + .expect("RegExp::initialize must return a RegExp object") + .clone(); + + Ok(Self { inner: regexp }) + } + + /// Create a `JsRegExp` from a regular expression `JsObject` + #[inline] + pub fn from_object(object: JsObject, context: &mut Context) -> JsResult { + if object.borrow().is_regexp() { + Ok(Self { inner: object }) + } else { + context.throw_type_error("object is not a RegExp") + } + } + + /// Returns a boolean value for whether the `d` flag is present in `JsRegExp` flags + #[inline] + pub fn has_indices(&self, context: &mut Context) -> JsResult { + RegExp::get_has_indices(&self.inner.clone().into(), &[], context) + .map(|v| v.as_boolean().expect("value must be a bool")) + } + + /// Returns a boolean value for whether the `g` flag is present in `JsRegExp` flags + #[inline] + pub fn global(&self, context: &mut Context) -> JsResult { + RegExp::get_global(&self.inner.clone().into(), &[], context) + .map(|v| v.as_boolean().expect("value must be a bool")) + } + + /// Returns a boolean value for whether the `i` flag is present in `JsRegExp` flags + #[inline] + pub fn ignore_case(&self, context: &mut Context) -> JsResult { + RegExp::get_ignore_case(&self.inner.clone().into(), &[], context) + .map(|v| v.as_boolean().expect("value must be a bool")) + } + + /// Returns a boolean value for whether the `m` flag is present in `JsRegExp` flags + #[inline] + pub fn multiline(&self, context: &mut Context) -> JsResult { + RegExp::get_multiline(&self.inner.clone().into(), &[], context) + .map(|v| v.as_boolean().expect("value must be a bool")) + } + + /// Returns a boolean value for whether the `s` flag is present in `JsRegExp` flags + #[inline] + pub fn dot_all(&self, context: &mut Context) -> JsResult { + RegExp::get_dot_all(&self.inner.clone().into(), &[], context) + .map(|v| v.as_boolean().expect("value must be a bool")) + } + + /// Returns a boolean value for whether the `u` flag is present in `JsRegExp` flags + #[inline] + pub fn unicode(&self, context: &mut Context) -> JsResult { + RegExp::get_unicode(&self.inner.clone().into(), &[], context) + .map(|v| v.as_boolean().expect("value must be a bool")) + } + + /// Returns a boolean value for whether the `y` flag is present in `JsRegExp` flags + #[inline] + pub fn sticky(&self, context: &mut Context) -> JsResult { + RegExp::get_sticky(&self.inner.clone().into(), &[], context) + .map(|v| v.as_boolean().expect("value must be a bool")) + } + + /// Returns the flags of `JsRegExp` as a string + /// ``` + /// # use boa_engine::{ + /// # object::builtins::JsRegExp, + /// # Context, JsValue, + /// # }; + /// # let context = &mut Context::default(); + /// let regexp = JsRegExp::new("foo", "gi", context).unwrap(); + /// + /// let flags = regexp.flags(context).unwrap(); + /// assert_eq!(flags, String::from("gi")); + /// ``` + #[inline] + pub fn flags(&self, context: &mut Context) -> JsResult { + RegExp::get_flags(&self.inner.clone().into(), &[], context).map(|v| { + v.as_string() + .expect("value must be string") + .to_std_string() + .expect("flags must be a valid string") + }) + } + + /// Returns the source pattern of `JsRegExp` as a string + /// ``` + /// # use boa_engine::{ + /// # object::builtins::JsRegExp, + /// # Context, JsValue, + /// # }; + /// # let context = &mut Context::default(); + /// let regexp = JsRegExp::new("foo", "gi", context).unwrap(); + /// + /// let src = regexp.source(context).unwrap(); + /// assert_eq!(src, String::from("foo")); + /// ``` + #[inline] + pub fn source(&self, context: &mut Context) -> JsResult { + RegExp::get_source(&self.inner.clone().into(), &[], context).map(|v| { + v.as_string() + .expect("value must be string") + .to_std_string() + .expect("source must be a valid string") + }) + } + + /// Executes a search for a match between `JsRegExp` and the provided string + /// ``` + /// # use boa_engine::{ + /// # object::builtins::JsRegExp, + /// # Context, JsValue, + /// # }; + /// # let context = &mut Context::default(); + /// let regexp = JsRegExp::new("foo", "gi", context).unwrap(); + /// + /// let test_result = regexp.test("football", context).unwrap(); + /// assert!(test_result); + /// ``` + #[inline] + pub fn test(&self, search_string: S, context: &mut Context) -> JsResult + where + S: Into, + { + RegExp::test(&self.inner.clone().into(), &[search_string.into()], context) + .map(|v| v.as_boolean().expect("value must be a bool")) + } + + /// Executes a search for a match in a specified string + /// + /// Returns a `JsArray` containing matched value and updates the `lastIndex` property, or `None` + #[inline] + pub fn exec(&self, search_string: S, context: &mut Context) -> JsResult> + where + S: Into, + { + RegExp::exec(&self.inner.clone().into(), &[search_string.into()], context).map(|v| { + if v.is_null() { + None + } else { + Some( + JsArray::from_object( + v.to_object(context).expect("v must be an array"), + context, + ) + .expect("from_object must not fail if v is an array object"), + ) + } + }) + } + + /// Return a string representing the regular expression. + /// ``` + /// # use boa_engine::{ + /// # object::builtins::JsRegExp, + /// # Context, JsValue, + /// # }; + /// # let context = &mut Context::default(); + /// let regexp = JsRegExp::new("foo", "gi", context).unwrap(); + /// + /// let to_string = regexp.to_string(context).unwrap(); + /// assert_eq!(to_string, String::from("/foo/gi")); + /// ``` + #[inline] + pub fn to_string(&self, context: &mut Context) -> JsResult { + RegExp::to_string(&self.inner.clone().into(), &[], context).map(|v| { + v.as_string() + .expect("value must be a string") + .to_std_string() + .expect("to_string value must be a valid string") + }) + } +} + +impl From for JsObject { + #[inline] + fn from(o: JsRegExp) -> Self { + o.inner.clone() + } +} + +impl From for JsValue { + #[inline] + fn from(o: JsRegExp) -> Self { + o.inner.clone().into() + } +} + +impl Deref for JsRegExp { + type Target = JsObject; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl JsObjectType for JsRegExp {} diff --git a/boa_engine/src/object/builtins/mod.rs b/boa_engine/src/object/builtins/mod.rs index 5c1b24b3d2b..c50548dbf75 100644 --- a/boa_engine/src/object/builtins/mod.rs +++ b/boa_engine/src/object/builtins/mod.rs @@ -7,6 +7,7 @@ mod jsfunction; mod jsmap; mod jsmap_iterator; pub(crate) mod jsproxy; +mod jsregexp; mod jsset; mod jsset_iterator; mod jstypedarray; @@ -18,6 +19,7 @@ pub use jsfunction::*; pub use jsmap::*; pub use jsmap_iterator::*; pub use jsproxy::{JsProxy, JsRevocableProxy}; +pub use jsregexp::JsRegExp; pub use jsset::*; pub use jsset_iterator::*; pub use jstypedarray::*; diff --git a/boa_examples/src/bin/jsregexp.rs b/boa_examples/src/bin/jsregexp.rs new file mode 100644 index 00000000000..474788eafab --- /dev/null +++ b/boa_examples/src/bin/jsregexp.rs @@ -0,0 +1,21 @@ +use boa_engine::{object::builtins::JsRegExp, Context, JsResult}; + +fn main() -> JsResult<()> { + let context = &mut Context::default(); + + let regexp = JsRegExp::new("foo", "gi", context)?; + + let test_result = regexp.test("football", context)?; + assert!(test_result); + + let flags = regexp.flags(context)?; + assert_eq!(flags, String::from("gi")); + + let src = regexp.source(context)?; + assert_eq!(src, String::from("foo")); + + let to_string = regexp.to_string(context)?; + assert_eq!(to_string, String::from("/foo/gi")); + + Ok(()) +}