diff --git a/docs/reference/src/language/builtins/namespaces.md b/docs/reference/src/language/builtins/namespaces.md index 3ce287b21f2..f852a3d5804 100644 --- a/docs/reference/src/language/builtins/namespaces.md +++ b/docs/reference/src/language/builtins/namespaces.md @@ -135,3 +135,53 @@ Square root ### `pow(float, float) -> float` Return the value of the first value raised to the second + +## `String` + +### `contains(string, string) -> bool` + +Return true if the first string contains the second string. + +### `starts-with(string, string) -> bool` + +Return true if the first string starts with the second string. + +### `ends-with(string, string) -> bool` + +Return true if the first string ends with the second string. + +### `slice(string, int, int) -> string` + +Return a substring of the first string, starting at the first index and ending at the second index. + +### `slice-by-length(string, int, int) -> string` + +Return a substring of the first string, starting at the first index and includes the number of characters specified by the second index. + +### `replace-first(string, string, string) -> string` + +Return a new string with the first string replaced by the second string. + +### `replace-last(string, string, string) -> string` + +Return a new string with the last occurrence of the second string replaced by the third string. + +### `replace-nth(string, string, string, int) -> string` + +Return a new string with the nth occurrence of the second string replaced by the third string. + +### `replace-all(string, string, string) -> string` + +Return a new string with all occurrences of the second string replaced by the third string. + +### `trim(string) -> string` + +Removes whitespace from both ends of the string including newlines, tabs, and spaces. + +### `trim-start(string) -> string` + +Removes whitespace from the start of the string including newlines, tabs, and spaces. + +### `trim-end(string) -> string` + +Removes whitespace from the end of the string including newlines, tabs, and spaces. diff --git a/internal/compiler/expression_tree.rs b/internal/compiler/expression_tree.rs index 25b927cd2a6..6419d988dc6 100644 --- a/internal/compiler/expression_tree.rs +++ b/internal/compiler/expression_tree.rs @@ -39,6 +39,18 @@ pub enum BuiltinFunction { ATan2, Log, Pow, + ReplaceFirst, + ReplaceNth, + ReplaceLast, + ReplaceAll, + Contains, + StartsWith, + EndsWith, + Slice, + SliceByLen, + Trim, + TrimStart, + TrimEnd, SetFocusItem, ClearFocusItem, ShowPopupWindow, @@ -147,6 +159,29 @@ impl BuiltinFunction { return_type: Box::new(Type::Float32), args: vec![Type::Float32, Type::Float32], }, + BuiltinFunction::ReplaceFirst + | BuiltinFunction::ReplaceLast + | BuiltinFunction::ReplaceAll => Type::Function { + return_type: Box::new(Type::String), + args: vec![Type::String, Type::String, Type::String], + }, + BuiltinFunction::ReplaceNth => Type::Function { + return_type: Box::new(Type::String), + args: vec![Type::String, Type::String, Type::String, Type::Int32], + }, + BuiltinFunction::Contains | BuiltinFunction::StartsWith | BuiltinFunction::EndsWith => { + Type::Function { + return_type: Box::new(Type::Bool), + args: vec![Type::String, Type::String], + } + } + BuiltinFunction::Slice | BuiltinFunction::SliceByLen => Type::Function { + return_type: Box::new(Type::String), + args: vec![Type::String, Type::Int32, Type::Int32], + }, + BuiltinFunction::Trim | BuiltinFunction::TrimStart | BuiltinFunction::TrimEnd => { + Type::Function { return_type: Box::new(Type::String), args: vec![Type::String] } + } BuiltinFunction::SetFocusItem => Type::Function { return_type: Box::new(Type::Void), args: vec![Type::ElementReference], @@ -355,6 +390,18 @@ impl BuiltinFunction { | BuiltinFunction::Pow | BuiltinFunction::ATan | BuiltinFunction::ATan2 => true, + BuiltinFunction::ReplaceFirst + | BuiltinFunction::ReplaceLast + | BuiltinFunction::ReplaceNth + | BuiltinFunction::ReplaceAll + | BuiltinFunction::Contains + | BuiltinFunction::StartsWith + | BuiltinFunction::EndsWith + | BuiltinFunction::Slice + | BuiltinFunction::SliceByLen + | BuiltinFunction::Trim + | BuiltinFunction::TrimStart + | BuiltinFunction::TrimEnd => false, BuiltinFunction::SetFocusItem | BuiltinFunction::ClearFocusItem => false, BuiltinFunction::ShowPopupWindow | BuiltinFunction::ClosePopupWindow => false, BuiltinFunction::SetSelectionOffsets => false, @@ -422,6 +469,18 @@ impl BuiltinFunction { | BuiltinFunction::Pow | BuiltinFunction::ATan | BuiltinFunction::ATan2 => true, + BuiltinFunction::ReplaceFirst + | BuiltinFunction::ReplaceLast + | BuiltinFunction::ReplaceNth + | BuiltinFunction::ReplaceAll + | BuiltinFunction::Contains + | BuiltinFunction::StartsWith + | BuiltinFunction::EndsWith + | BuiltinFunction::Slice + | BuiltinFunction::SliceByLen + | BuiltinFunction::Trim + | BuiltinFunction::TrimStart + | BuiltinFunction::TrimEnd => false, BuiltinFunction::SetFocusItem | BuiltinFunction::ClearFocusItem => false, BuiltinFunction::ShowPopupWindow | BuiltinFunction::ClosePopupWindow => false, BuiltinFunction::SetSelectionOffsets => false, diff --git a/internal/compiler/generator/cpp.rs b/internal/compiler/generator/cpp.rs index 6cc3d0650d2..39e3975d9c6 100644 --- a/internal/compiler/generator/cpp.rs +++ b/internal/compiler/generator/cpp.rs @@ -485,6 +485,7 @@ struct ConditionalIncludes { iostream: Cell, cstdlib: Cell, cmath: Cell, + string: Cell, } #[derive(Clone)] @@ -3442,6 +3443,54 @@ fn compile_builtin_function_call( ctx.generator_state.conditional_includes.cmath.set(true); format!("std::atan2({}, {}) / {}", a.next().unwrap(), a.next().unwrap(), pi_180) } + BuiltinFunction::ReplaceFirst => { + ctx.generator_state.conditional_includes.string.set(true); + format!("slint::private_api::replace_first({}, {}, {})", a.next().unwrap(), a.next().unwrap(), a.next().unwrap()) + } + BuiltinFunction::ReplaceLast => { + ctx.generator_state.conditional_includes.string.set(true); + format!("slint::private_api::replace_last({}, {}, {})", a.next().unwrap(), a.next().unwrap(), a.next().unwrap()) + } + BuiltinFunction::ReplaceNth => { + ctx.generator_state.conditional_includes.string.set(true); + format!("slint::private_api::replace_nth({}, {}, {}, {})", a.next().unwrap(), a.next().unwrap(), a.next().unwrap(), a.next().unwrap()) + } + BuiltinFunction::ReplaceAll => { + ctx.generator_state.conditional_includes.string.set(true); + format!("slint::private_api::replace_all({}, {}, {})", a.next().unwrap(), a.next().unwrap(), a.next().unwrap()) + } + BuiltinFunction::Contains => { + ctx.generator_state.conditional_includes.string.set(true); + format!("slint::private_api::contains({}, {})", a.next().unwrap(), a.next().unwrap()) + } + BuiltinFunction::StartsWith => { + ctx.generator_state.conditional_includes.string.set(true); + format!("slint::private_api::starts_with({}, {})", a.next().unwrap(), a.next().unwrap()) + } + BuiltinFunction::EndsWith => { + ctx.generator_state.conditional_includes.string.set(true); + format!("slint::private_api::ends_with({}, {})", a.next().unwrap(), a.next().unwrap()) + } + BuiltinFunction::Trim => { + ctx.generator_state.conditional_includes.string.set(true); + format!("slint::private_api::trim({})", a.next().unwrap()) + } + BuiltinFunction::TrimStart => { + ctx.generator_state.conditional_includes.string.set(true); + format!("slint::private_api::trim_start({})", a.next().unwrap()) + } + BuiltinFunction::TrimEnd => { + ctx.generator_state.conditional_includes.string.set(true); + format!("slint::private_api::trim_end({})", a.next().unwrap()) + } + BuiltinFunction::SliceByLen => { + ctx.generator_state.conditional_includes.string.set(true); + format!("slint::private_api::substr({}, {}, {})", a.next().unwrap(), a.next().unwrap(), a.next().unwrap()) + } + BuiltinFunction::Slice => { + ctx.generator_state.conditional_includes.string.set(true); + format!("slint::private_api::substring({}, {}, {})", a.next().unwrap(), a.next().unwrap(), a.next().unwrap()) + } BuiltinFunction::SetFocusItem => { if let [llr::Expression::PropertyReference(pr)] = arguments { let window = access_window_field(ctx); diff --git a/internal/compiler/generator/rust.rs b/internal/compiler/generator/rust.rs index 75ccc3c3290..41aa2bf7253 100644 --- a/internal/compiler/generator/rust.rs +++ b/internal/compiler/generator/rust.rs @@ -2781,6 +2781,46 @@ fn compile_builtin_function_call( BuiltinFunction::StringToFloat => { quote!(#(#a)*.as_str().parse::().unwrap_or_default()) } + BuiltinFunction::ReplaceFirst => { + let (s, from, to) = (a.next().unwrap(), a.next().unwrap(), a.next().unwrap()); + quote!(#s.replace_first(#from.as_str(), #to.as_str())) + } + BuiltinFunction::ReplaceLast => { + let (s, from, to) = (a.next().unwrap(), a.next().unwrap(), a.next().unwrap()); + quote!(#s.replace_last(#from.as_str(), #to.as_str())) + } + BuiltinFunction::ReplaceNth => { + let (s, from, to, n) = + (a.next().unwrap(), a.next().unwrap(), a.next().unwrap(), a.next().unwrap()); + quote!(#s.replace_nth(#from.as_str(), #to.as_str(), #n as usize)) + } + BuiltinFunction::ReplaceAll => { + let (s, from, to) = (a.next().unwrap(), a.next().unwrap(), a.next().unwrap()); + quote!(#s.replace_all(#from.as_str(), #to.as_str())) + } + BuiltinFunction::Contains => { + let (s, sub) = (a.next().unwrap(), a.next().unwrap()); + quote!(#s.as_str().contains(#sub.as_str())) + } + BuiltinFunction::StartsWith => { + let (s, sub) = (a.next().unwrap(), a.next().unwrap()); + quote!(#s.starts_with_str(#sub.as_str())) + } + BuiltinFunction::EndsWith => { + let (s, sub) = (a.next().unwrap(), a.next().unwrap()); + quote!(#s.ends_with_str(#sub.as_str())) + } + BuiltinFunction::Slice => { + let (s, start, end) = (a.next().unwrap(), a.next().unwrap(), a.next().unwrap()); + quote!(#s.slice(#start as usize, #end as usize)) + } + BuiltinFunction::SliceByLen => { + let (s, start, len) = (a.next().unwrap(), a.next().unwrap(), a.next().unwrap()); + quote!(#s.slice_by_len(#start as usize, #len as usize)) + } + BuiltinFunction::Trim => quote!(#(#a)*.trim()), + BuiltinFunction::TrimStart => quote!(#(#a)*.trim_start()), + BuiltinFunction::TrimEnd => quote!(#(#a)*.trim_end()), BuiltinFunction::StringIsFloat => quote!(#(#a)*.as_str().parse::().is_ok()), BuiltinFunction::ColorRgbaStruct => quote!( #(#a)*.to_argb_u8()), BuiltinFunction::ColorHsvaStruct => quote!( #(#a)*.to_hsva()), diff --git a/internal/compiler/llr/optim_passes/inline_expressions.rs b/internal/compiler/llr/optim_passes/inline_expressions.rs index 7e8f0d130dd..840052ef4ea 100644 --- a/internal/compiler/llr/optim_passes/inline_expressions.rs +++ b/internal/compiler/llr/optim_passes/inline_expressions.rs @@ -95,6 +95,18 @@ fn builtin_function_cost(function: &BuiltinFunction) -> isize { BuiltinFunction::ATan2 => 10, BuiltinFunction::Log => 10, BuiltinFunction::Pow => 10, + BuiltinFunction::ReplaceFirst + | BuiltinFunction::ReplaceLast + | BuiltinFunction::ReplaceNth + | BuiltinFunction::ReplaceAll + | BuiltinFunction::Contains + | BuiltinFunction::StartsWith + | BuiltinFunction::EndsWith + | BuiltinFunction::Slice + | BuiltinFunction::SliceByLen + | BuiltinFunction::Trim + | BuiltinFunction::TrimStart + | BuiltinFunction::TrimEnd => 10, BuiltinFunction::SetFocusItem | BuiltinFunction::ClearFocusItem => isize::MAX, BuiltinFunction::ShowPopupWindow | BuiltinFunction::ClosePopupWindow => isize::MAX, BuiltinFunction::SetSelectionOffsets => isize::MAX, diff --git a/internal/compiler/lookup.rs b/internal/compiler/lookup.rs index 4b925bd6b50..ccfcf4dc84f 100644 --- a/internal/compiler/lookup.rs +++ b/internal/compiler/lookup.rs @@ -102,6 +102,7 @@ pub enum LookupResult { pub enum BuiltinNamespace { Colors, Math, + String, Key, SlintInternal, } @@ -165,6 +166,9 @@ impl LookupObject for LookupResult { (ColorSpecific, ColorFunctions).for_each_entry(ctx, f) } LookupResult::Namespace(BuiltinNamespace::Math) => MathFunctions.for_each_entry(ctx, f), + LookupResult::Namespace(BuiltinNamespace::String) => { + StringFunctions.for_each_entry(ctx, f) + } LookupResult::Namespace(BuiltinNamespace::Key) => KeysLookup.for_each_entry(ctx, f), LookupResult::Namespace(BuiltinNamespace::SlintInternal) => { SlintInternal.for_each_entry(ctx, f) @@ -180,6 +184,7 @@ impl LookupObject for LookupResult { (ColorSpecific, ColorFunctions).lookup(ctx, name) } LookupResult::Namespace(BuiltinNamespace::Math) => MathFunctions.lookup(ctx, name), + LookupResult::Namespace(BuiltinNamespace::String) => StringFunctions.lookup(ctx, name), LookupResult::Namespace(BuiltinNamespace::Key) => KeysLookup.lookup(ctx, name), LookupResult::Namespace(BuiltinNamespace::SlintInternal) => { SlintInternal.lookup(ctx, name) @@ -810,6 +815,36 @@ impl LookupObject for MathFunctions { } } +struct StringFunctions; +impl LookupObject for StringFunctions { + fn for_each_entry( + &self, + ctx: &LookupCtx, + f: &mut impl FnMut(&str, LookupResult) -> Option, + ) -> Option { + use Expression::BuiltinFunctionReference; + let t = &ctx.current_token; + let sl = || t.as_ref().map(|t| t.to_source_location()); + let mut f = |n, e: Expression| f(n, e.into()); + None.or_else(|| { + f("replace-first", BuiltinFunctionReference(BuiltinFunction::ReplaceFirst, sl())) + }) + .or_else(|| f("replace-last", BuiltinFunctionReference(BuiltinFunction::ReplaceLast, sl()))) + .or_else(|| f("replace-nth", BuiltinFunctionReference(BuiltinFunction::ReplaceNth, sl()))) + .or_else(|| f("replace-all", BuiltinFunctionReference(BuiltinFunction::ReplaceAll, sl()))) + .or_else(|| f("contains", BuiltinFunctionReference(BuiltinFunction::Contains, sl()))) + .or_else(|| f("slice", BuiltinFunctionReference(BuiltinFunction::Slice, sl()))) + .or_else(|| { + f("slice-by-length", BuiltinFunctionReference(BuiltinFunction::SliceByLen, sl())) + }) + .or_else(|| f("starts-with", BuiltinFunctionReference(BuiltinFunction::StartsWith, sl()))) + .or_else(|| f("ends-with", BuiltinFunctionReference(BuiltinFunction::EndsWith, sl()))) + .or_else(|| f("trim", BuiltinFunctionReference(BuiltinFunction::Trim, sl()))) + .or_else(|| f("trim-start", BuiltinFunctionReference(BuiltinFunction::TrimStart, sl()))) + .or_else(|| f("trim-end", BuiltinFunctionReference(BuiltinFunction::TrimEnd, sl()))) + } +} + struct SlintInternal; impl LookupObject for SlintInternal { fn for_each_entry( @@ -882,7 +917,7 @@ impl LookupObject for BuiltinFunctionLookup { ctx: &LookupCtx, f: &mut impl FnMut(&str, LookupResult) -> Option, ) -> Option { - (MathFunctions, ColorFunctions) + ((MathFunctions, ColorFunctions), StringFunctions) .for_each_entry(ctx, f) .or_else(|| { f( diff --git a/internal/core/string.rs b/internal/core/string.rs index de8e584b020..ac63a2b2dda 100644 --- a/internal/core/string.rs +++ b/internal/core/string.rs @@ -96,6 +96,213 @@ impl SharedString { self.inner.make_mut_slice()[prev_len] = first; } } + + /// Replaces the first occurrence of `find` with `replace`. + /// + /// ``` + /// # use i_slint_core::SharedString; + /// let hello = SharedString::from("Hello, World!"); + /// assert_eq!(hello.replace_first("World", "Universe"), "Hello, Universe!"); + /// assert_eq!(hello.replace_first("o", "0"), "Hell0, World!"); + /// assert_eq!(hello.replace_first("l", "L"), "HeLlo, World!"); + /// ``` + pub fn replace_first(&self, find: &str, replace: &str) -> SharedString { + let mut result = SharedString::default(); + let mut iter = self.as_str().splitn(2, find); + if let Some(first) = iter.next() { + result.push_str(first); + if let Some(second) = iter.next() { + result.push_str(replace); + result.push_str(second); + } + } + result + } + + /// Replaces the last occurrence of `find` with `replace`. + /// + /// ``` + /// # use i_slint_core::SharedString; + /// let hello = SharedString::from("Hello, World!"); + /// assert_eq!(hello.replace_last("o", "0"), "Hello, W0rld!"); + /// assert_eq!(hello.replace_last("World", "Universe"), "Hello, Universe!"); + /// ``` + pub fn replace_last(&self, find: &str, replace: &str) -> SharedString { + // Find the last occurrence of `find` in the string and replace it with `replace` + let mut result = SharedString::default(); + let mut iter = self.as_str().rsplitn(2, find); + if let Some(after) = iter.next() { + if let Some(before) = iter.next() { + result.push_str(before); + result.push_str(replace); + } + result.push_str(after); + } + result + } + + /// Replaces the nth occurrence of `find` with `replace`. + /// + /// ``` + /// # use i_slint_core::SharedString; + /// let hello = SharedString::from("Hello, World!"); + /// assert_eq!(hello.replace_nth("l", "L", 2), "HelLo, World!"); + /// assert_eq!(hello.replace_nth("o", "0", 1), "Hell0, World!"); + /// assert_eq!(hello.replace_nth("o", "ooo...", 1), "Hellooo..., World!"); + /// ``` + pub fn replace_nth(&self, find: &str, replace: &str, nth: usize) -> SharedString { + let mut result = SharedString::default(); + let mut iter = self.as_str().splitn(nth + 1, find); + if let Some(first) = iter.next() { + result.push_str(first); + for (i, part) in iter.enumerate() { + if i == nth - 1 { + result.push_str(replace); + } else { + result.push_str(find); + } + result.push_str(part); + } + } + result + } + + /// Replaces all occurrences of `find` with `replace`. + /// + /// ``` + /// # use i_slint_core::SharedString; + /// let hello = SharedString::from("Hello, World!"); + /// assert_eq!(hello.replace_all("o", "0"), "Hell0, W0rld!"); + /// ``` + pub fn replace_all(&self, find: &str, replace: &str) -> SharedString { + let mut result = SharedString::default(); + let mut iter = self.as_str().split(find); + if let Some(first) = iter.next() { + result.push_str(first); + for part in iter { + result.push_str(replace); + result.push_str(part); + } + } + result + } + + /// Whether the string starts with the given substring + /// + /// ``` + /// # use i_slint_core::SharedString; + /// let hello = SharedString::from("Hello, World!"); + /// assert_eq!(hello.starts_with_str("Hello"), true); + /// assert_eq!(hello.starts_with_str("World"), false); + /// ``` + pub fn starts_with_str(&self, x: &str) -> bool { + self.as_str().starts_with(x) + } + + /// Whether the string ends with the given substring + /// + /// ``` + /// # use i_slint_core::SharedString; + /// let hello = SharedString::from("Hello, World!"); + /// assert_eq!(hello.ends_with_str("World!"), true); + /// assert_eq!(hello.ends_with_str("Hello"), false); + /// ``` + pub fn ends_with_str(&self, x: &str) -> bool { + self.as_str().ends_with(x) + } + + /// Returns a substring of the string where the `start` is the index of the first character and `length` is the number of characters. + /// + /// ``` + /// # use i_slint_core::SharedString; + /// let hello = SharedString::from("Hello, World!"); + /// assert_eq!(hello.slice_by_len(7, 5), "World"); + /// assert_eq!(hello.slice_by_len(0, 5), "Hello"); + /// assert_eq!(hello.slice_by_len(0, 1000), "Hello, World!"); + /// assert_eq!(hello.slice_by_len(0, 0), ""); + /// assert_eq!(hello.slice_by_len(1000, 1000), ""); + /// assert_eq!(hello.slice_by_len(1, 3), "ell"); + /// assert_eq!(hello.slice_by_len(7, 1000), "World!"); + /// ``` + pub fn slice_by_len(&self, start: usize, length: usize) -> SharedString { + // if start is greater than the length of the string, return an empty string + // if length is 0, return an empty string + if start >= self.len() || length == 0 { + return SharedString::default(); + } + let end = start + length; + let end = core::cmp::min(end, self.len()); + let mut result = SharedString::default(); + result.push_str(&self.as_str()[start..end]); + result + } + + /// Returns a substring of the string where the `start` is the index of the first character and `end` is the index of the last character. + /// + /// ``` + /// # use i_slint_core::SharedString; + /// let hello = SharedString::from("Hello, World!"); + /// assert_eq!(hello.slice(7, 12), "World"); + /// assert_eq!(hello.slice(0, 5), "Hello"); + /// assert_eq!(hello.slice(0, 1000), "Hello, World!"); + /// assert_eq!(hello.slice(0, 0), ""); + /// assert_eq!(hello.slice(1000, 1000), ""); + /// assert_eq!(hello.slice(1, 3), "el"); + /// assert_eq!(hello.slice(7, 1000), "World!"); + /// ``` + pub fn slice(&self, start: usize, end: usize) -> SharedString { + if start > end { + return SharedString::default(); + } + self.slice_by_len(start, end - start) + } + + /// Removes leading and trailing whitespaces including new lines + /// + /// ``` + /// # use i_slint_core::SharedString; + /// let hello = SharedString::from(" Hello, World! "); + /// assert_eq!(hello.trim(), "Hello, World!"); + /// // New lines are also removed + /// let hello = SharedString::from("\nHello, World!\n\n"); + /// assert_eq!(hello.trim(), "Hello, World!"); + /// ``` + pub fn trim(&self) -> SharedString { + let mut result = SharedString::default(); + result.push_str(self.as_str().trim()); + result + } + + /// Removes leading whitespaces including new lines + /// ``` + /// # use i_slint_core::SharedString; + /// let hello = SharedString::from(" Hello, World! "); + /// assert_eq!(hello.trim_start(), "Hello, World! "); + /// // New lines are also removed + /// let hello = SharedString::from("\nHello, World!\n\n"); + /// assert_eq!(hello.trim_start(), "Hello, World!\n\n"); + /// ``` + pub fn trim_start(&self) -> SharedString { + let mut result = SharedString::default(); + result.push_str(self.as_str().trim_start()); + result + } + + /// Removes trailing whitespaces including new lines + /// + /// ``` + /// # use i_slint_core::SharedString; + /// let hello = SharedString::from(" Hello, World! "); + /// assert_eq!(hello.trim_end(), " Hello, World!"); + /// // New lines are also removed + /// let hello = SharedString::from("\nHello, World!\n\n"); + /// assert_eq!(hello.trim_end(), "\nHello, World!"); + /// ``` + pub fn trim_end(&self) -> SharedString { + let mut result = SharedString::default(); + result.push_str(self.as_str().trim_end()); + result + } } impl Deref for SharedString { diff --git a/internal/interpreter/eval.rs b/internal/interpreter/eval.rs index 2266516e93c..6b8351aef25 100644 --- a/internal/interpreter/eval.rs +++ b/internal/interpreter/eval.rs @@ -495,6 +495,86 @@ fn call_builtin_function( let y: f64 = eval_expression(&arguments[1], local_context).try_into().unwrap(); Value::Number(x.powf(y)) } + BuiltinFunction::ReplaceFirst => { + // Replace a string with another string replace("hello world", "hello", "goodbye") + let s: SharedString = eval_expression(&arguments[0], local_context).try_into().unwrap(); + let from: SharedString = + eval_expression(&arguments[1], local_context).try_into().unwrap(); + let to: SharedString = + eval_expression(&arguments[2], local_context).try_into().unwrap(); + Value::String(s.replace_first(from.as_str(), to.as_str())) + } + BuiltinFunction::ReplaceLast => { + // Replace the last occurrence of a string with another string replace("hello world", "hello", "goodbye") + let s: SharedString = eval_expression(&arguments[0], local_context).try_into().unwrap(); + let from: SharedString = + eval_expression(&arguments[1], local_context).try_into().unwrap(); + let to: SharedString = + eval_expression(&arguments[2], local_context).try_into().unwrap(); + Value::String(s.replace_last(from.as_str(), to.as_str())) + } + BuiltinFunction::ReplaceNth => { + // Replace the nth occurrence of a string with another string replace("hello world", "hello", "goodbye", 2) + let s: SharedString = eval_expression(&arguments[0], local_context).try_into().unwrap(); + let from: SharedString = + eval_expression(&arguments[1], local_context).try_into().unwrap(); + let to: SharedString = + eval_expression(&arguments[2], local_context).try_into().unwrap(); + let n: usize = eval_expression(&arguments[3], local_context).try_into().unwrap(); + Value::String(s.replace_nth(from.as_str(), to.as_str(), n)) + } + BuiltinFunction::ReplaceAll => { + // Replace all occurrences of a string with another string replace("hello world", "hello", "goodbye") + let s: SharedString = eval_expression(&arguments[0], local_context).try_into().unwrap(); + let from: SharedString = + eval_expression(&arguments[1], local_context).try_into().unwrap(); + let to: SharedString = + eval_expression(&arguments[2], local_context).try_into().unwrap(); + let result = s.replace_all(from.as_str(), to.as_str()); + Value::String(result) + } + BuiltinFunction::Contains => { + let s: SharedString = eval_expression(&arguments[0], local_context).try_into().unwrap(); + let search: SharedString = + eval_expression(&arguments[1], local_context).try_into().unwrap(); + Value::Bool(s.contains(search.as_str())) + } + BuiltinFunction::StartsWith => { + let s: SharedString = eval_expression(&arguments[0], local_context).try_into().unwrap(); + let search: SharedString = + eval_expression(&arguments[1], local_context).try_into().unwrap(); + Value::Bool(s.starts_with_str(search.as_str())) + } + BuiltinFunction::EndsWith => { + let s: SharedString = eval_expression(&arguments[0], local_context).try_into().unwrap(); + let search: SharedString = + eval_expression(&arguments[1], local_context).try_into().unwrap(); + Value::Bool(s.starts_with_str(search.as_str())) + } + BuiltinFunction::Slice => { + let s: SharedString = eval_expression(&arguments[0], local_context).try_into().unwrap(); + let start: usize = eval_expression(&arguments[1], local_context).try_into().unwrap(); + let end: usize = eval_expression(&arguments[2], local_context).try_into().unwrap(); + Value::String(s.slice(start, end)) + } + BuiltinFunction::SliceByLen => { + let s: SharedString = eval_expression(&arguments[0], local_context).try_into().unwrap(); + let start: usize = eval_expression(&arguments[1], local_context).try_into().unwrap(); + let len: usize = eval_expression(&arguments[2], local_context).try_into().unwrap(); + Value::String(s.slice_by_len(start, len)) + } + BuiltinFunction::Trim => { + let s: SharedString = eval_expression(&arguments[0], local_context).try_into().unwrap(); + Value::String(s.trim().into()) + } + BuiltinFunction::TrimStart => { + let s: SharedString = eval_expression(&arguments[0], local_context).try_into().unwrap(); + Value::String(s.trim_start().into()) + } + BuiltinFunction::TrimEnd => { + let s: SharedString = eval_expression(&arguments[0], local_context).try_into().unwrap(); + Value::String(s.trim_end().into()) + } BuiltinFunction::SetFocusItem => { if arguments.len() != 1 { panic!("internal error: incorrect argument count to SetFocusItem")