Skip to main content

jpx_core/extensions/
string.rs

1//! String manipulation functions.
2
3use std::collections::HashSet;
4
5use heck::{
6    ToKebabCase, ToLowerCamelCase, ToShoutyKebabCase, ToShoutySnakeCase, ToSnakeCase, ToTitleCase,
7    ToTrainCase, ToUpperCamelCase,
8};
9use regex::Regex;
10use serde_json::{Number, Value};
11
12use crate::functions::{Function, custom_error};
13use crate::interpreter::SearchResult;
14use crate::registry::register_if_enabled;
15use crate::{Context, Runtime, arg, defn};
16
17// =============================================================================
18// lower(string) -> string
19// =============================================================================
20
21defn!(LowerFn, vec![arg!(string)], None);
22
23impl Function for LowerFn {
24    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
25        self.signature.validate(args, ctx)?;
26        let s = args[0]
27            .as_str()
28            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
29        Ok(Value::String(s.to_lowercase()))
30    }
31}
32
33// =============================================================================
34// upper(string) -> string
35// =============================================================================
36
37defn!(UpperFn, vec![arg!(string)], None);
38
39impl Function for UpperFn {
40    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
41        self.signature.validate(args, ctx)?;
42        let s = args[0]
43            .as_str()
44            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
45        Ok(Value::String(s.to_uppercase()))
46    }
47}
48
49// =============================================================================
50// trim(string) -> string
51// =============================================================================
52
53defn!(TrimFn, vec![arg!(string)], None);
54
55impl Function for TrimFn {
56    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
57        self.signature.validate(args, ctx)?;
58        let s = args[0]
59            .as_str()
60            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
61        Ok(Value::String(s.trim().to_string()))
62    }
63}
64
65// =============================================================================
66// trim_start(string) -> string
67// =============================================================================
68
69defn!(TrimStartFn, vec![arg!(string)], None);
70
71impl Function for TrimStartFn {
72    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
73        self.signature.validate(args, ctx)?;
74        let s = args[0]
75            .as_str()
76            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
77        Ok(Value::String(s.trim_start().to_string()))
78    }
79}
80
81// =============================================================================
82// trim_end(string) -> string
83// =============================================================================
84
85defn!(TrimEndFn, vec![arg!(string)], None);
86
87impl Function for TrimEndFn {
88    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
89        self.signature.validate(args, ctx)?;
90        let s = args[0]
91            .as_str()
92            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
93        Ok(Value::String(s.trim_end().to_string()))
94    }
95}
96
97// =============================================================================
98// split(string, delimiter) -> array
99// =============================================================================
100
101defn!(SplitFn, vec![arg!(string), arg!(string)], None);
102
103impl Function for SplitFn {
104    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
105        self.signature.validate(args, ctx)?;
106        let s = args[0]
107            .as_str()
108            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
109        let delimiter = args[1]
110            .as_str()
111            .ok_or_else(|| custom_error(ctx, "Expected string delimiter"))?;
112
113        let parts: Vec<Value> = s
114            .split(delimiter)
115            .map(|part| Value::String(part.to_string()))
116            .collect();
117
118        Ok(Value::Array(parts))
119    }
120}
121
122// =============================================================================
123// replace(string, old, new) -> string
124// =============================================================================
125
126defn!(
127    ReplaceFn,
128    vec![arg!(string), arg!(string), arg!(string)],
129    None
130);
131
132impl Function for ReplaceFn {
133    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
134        self.signature.validate(args, ctx)?;
135        let s = args[0]
136            .as_str()
137            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
138        let old = args[1]
139            .as_str()
140            .ok_or_else(|| custom_error(ctx, "Expected old string argument"))?;
141        let new = args[2]
142            .as_str()
143            .ok_or_else(|| custom_error(ctx, "Expected new string argument"))?;
144
145        Ok(Value::String(s.replace(old, new)))
146    }
147}
148
149// =============================================================================
150// pad_left(string, width, char) -> string
151// =============================================================================
152
153defn!(
154    PadLeftFn,
155    vec![arg!(string), arg!(number), arg!(string)],
156    None
157);
158
159impl Function for PadLeftFn {
160    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
161        self.signature.validate(args, ctx)?;
162        let s = args[0]
163            .as_str()
164            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
165        let width = args[1]
166            .as_f64()
167            .map(|n| n as usize)
168            .ok_or_else(|| custom_error(ctx, "Expected positive number for width"))?;
169        let pad_char = args[2]
170            .as_str()
171            .ok_or_else(|| custom_error(ctx, "Expected string for pad character"))?;
172
173        let pad = pad_char.chars().next().unwrap_or(' ');
174        let result = if s.len() >= width {
175            s.to_string()
176        } else {
177            format!("{}{}", pad.to_string().repeat(width - s.len()), s)
178        };
179
180        Ok(Value::String(result))
181    }
182}
183
184// =============================================================================
185// pad_right(string, width, char) -> string
186// =============================================================================
187
188defn!(
189    PadRightFn,
190    vec![arg!(string), arg!(number), arg!(string)],
191    None
192);
193
194impl Function for PadRightFn {
195    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
196        self.signature.validate(args, ctx)?;
197        let s = args[0]
198            .as_str()
199            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
200        let width = args[1]
201            .as_f64()
202            .map(|n| n as usize)
203            .ok_or_else(|| custom_error(ctx, "Expected positive number for width"))?;
204        let pad_char = args[2]
205            .as_str()
206            .ok_or_else(|| custom_error(ctx, "Expected string for pad character"))?;
207
208        let pad = pad_char.chars().next().unwrap_or(' ');
209        let result = if s.len() >= width {
210            s.to_string()
211        } else {
212            format!("{}{}", s, pad.to_string().repeat(width - s.len()))
213        };
214
215        Ok(Value::String(result))
216    }
217}
218
219// =============================================================================
220// substr(string, start, length?) -> string
221// =============================================================================
222
223defn!(
224    SubstrFn,
225    vec![arg!(string), arg!(number)],
226    Some(arg!(number))
227);
228
229impl Function for SubstrFn {
230    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
231        self.signature.validate(args, ctx)?;
232        let s = args[0]
233            .as_str()
234            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
235        let start = args[1]
236            .as_f64()
237            .map(|n| n as i64)
238            .ok_or_else(|| custom_error(ctx, "Expected number for start"))?;
239
240        // Handle negative start (from end)
241        let start_idx = if start < 0 {
242            (s.len() as i64 + start).max(0) as usize
243        } else {
244            start as usize
245        };
246
247        let result = if args.len() > 2 {
248            let length = args[2]
249                .as_f64()
250                .map(|n| n as usize)
251                .ok_or_else(|| custom_error(ctx, "Expected positive number for length"))?;
252            s.chars().skip(start_idx).take(length).collect()
253        } else {
254            s.chars().skip(start_idx).collect()
255        };
256
257        Ok(Value::String(result))
258    }
259}
260
261// =============================================================================
262// capitalize(string) -> string (first letter uppercase)
263// =============================================================================
264
265defn!(CapitalizeFn, vec![arg!(string)], None);
266
267impl Function for CapitalizeFn {
268    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
269        self.signature.validate(args, ctx)?;
270        let s = args[0]
271            .as_str()
272            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
273
274        let result = if s.is_empty() {
275            String::new()
276        } else {
277            let mut chars = s.chars();
278            match chars.next() {
279                None => String::new(),
280                Some(first) => first.to_uppercase().to_string() + chars.as_str(),
281            }
282        };
283
284        Ok(Value::String(result))
285    }
286}
287
288// =============================================================================
289// title(string) -> string (capitalize each word)
290// =============================================================================
291
292defn!(TitleFn, vec![arg!(string)], None);
293
294impl Function for TitleFn {
295    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
296        self.signature.validate(args, ctx)?;
297        let s = args[0]
298            .as_str()
299            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
300
301        let result = s
302            .split_whitespace()
303            .map(|word| {
304                let mut chars = word.chars();
305                match chars.next() {
306                    None => String::new(),
307                    Some(first) => {
308                        first.to_uppercase().to_string() + &chars.as_str().to_lowercase()
309                    }
310                }
311            })
312            .collect::<Vec<_>>()
313            .join(" ");
314
315        Ok(Value::String(result))
316    }
317}
318
319// =============================================================================
320// repeat(string, count) -> string
321// =============================================================================
322
323defn!(RepeatFn, vec![arg!(string), arg!(number)], None);
324
325impl Function for RepeatFn {
326    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
327        self.signature.validate(args, ctx)?;
328        let s = args[0]
329            .as_str()
330            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
331        let count = args[1]
332            .as_f64()
333            .map(|n| n as usize)
334            .ok_or_else(|| custom_error(ctx, "Expected positive number for count"))?;
335
336        Ok(Value::String(s.repeat(count)))
337    }
338}
339
340// =============================================================================
341// index_of(string, search, start?, end?) -> number | null (JEP-014)
342// =============================================================================
343
344defn!(
345    IndexOfFn,
346    vec![arg!(string), arg!(string)],
347    Some(arg!(number))
348);
349
350/// Helper to normalize an index (handling negative values) within a string length
351fn normalize_index(idx: i64, len: usize) -> usize {
352    if idx < 0 {
353        (len as i64 + idx).max(0) as usize
354    } else {
355        (idx as usize).min(len)
356    }
357}
358
359impl Function for IndexOfFn {
360    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
361        self.signature.validate(args, ctx)?;
362        let s = args[0]
363            .as_str()
364            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
365        let search = args[1]
366            .as_str()
367            .ok_or_else(|| custom_error(ctx, "Expected search string"))?;
368
369        let len = s.len();
370
371        // Get optional start parameter (default: 0)
372        let start = if args.len() > 2 {
373            let start_val = args[2]
374                .as_f64()
375                .ok_or_else(|| custom_error(ctx, "Expected number for start"))?
376                as i64;
377            normalize_index(start_val, len)
378        } else {
379            0
380        };
381
382        // Get optional end parameter (default: string length)
383        let end = if args.len() > 3 {
384            let end_val = args[3]
385                .as_f64()
386                .ok_or_else(|| custom_error(ctx, "Expected number for end"))?
387                as i64;
388            normalize_index(end_val, len)
389        } else {
390            len
391        };
392
393        // Search within the slice [start, end)
394        if start >= end || start >= len {
395            return Ok(Value::Null);
396        }
397
398        let slice = &s[start..end.min(len)];
399        match slice.find(search) {
400            Some(idx) => Ok(Value::Number(Number::from((start + idx) as i64))),
401            None => Ok(Value::Null),
402        }
403    }
404}
405
406// =============================================================================
407// last_index_of(string, search, start?, end?) -> number | null (JEP-014)
408// =============================================================================
409
410defn!(
411    LastIndexOfFn,
412    vec![arg!(string), arg!(string)],
413    Some(arg!(number))
414);
415
416impl Function for LastIndexOfFn {
417    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
418        self.signature.validate(args, ctx)?;
419        let s = args[0]
420            .as_str()
421            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
422        let search = args[1]
423            .as_str()
424            .ok_or_else(|| custom_error(ctx, "Expected search string"))?;
425
426        let len = s.len();
427
428        // Get optional start parameter (default: 0)
429        let start = if args.len() > 2 {
430            let start_val = args[2]
431                .as_f64()
432                .ok_or_else(|| custom_error(ctx, "Expected number for start"))?
433                as i64;
434            normalize_index(start_val, len)
435        } else {
436            0
437        };
438
439        // Get optional end parameter (default: string length)
440        let end = if args.len() > 3 {
441            let end_val = args[3]
442                .as_f64()
443                .ok_or_else(|| custom_error(ctx, "Expected number for end"))?
444                as i64;
445            normalize_index(end_val, len)
446        } else {
447            len
448        };
449
450        // Search within the slice [start, end)
451        if start >= end || start >= len {
452            return Ok(Value::Null);
453        }
454
455        let slice = &s[start..end.min(len)];
456        match slice.rfind(search) {
457            Some(idx) => Ok(Value::Number(Number::from((start + idx) as i64))),
458            None => Ok(Value::Null),
459        }
460    }
461}
462
463// =============================================================================
464// slice(string, start, end?) -> string
465// =============================================================================
466
467defn!(
468    SliceFn,
469    vec![arg!(string), arg!(number)],
470    Some(arg!(number))
471);
472
473impl Function for SliceFn {
474    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
475        self.signature.validate(args, ctx)?;
476        let s = args[0]
477            .as_str()
478            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
479
480        let len = s.len() as i64;
481
482        let start = args[1]
483            .as_f64()
484            .map(|n| n as i64)
485            .ok_or_else(|| custom_error(ctx, "Expected number for start"))?;
486
487        // Handle negative indices
488        let start_idx = if start < 0 {
489            (len + start).max(0) as usize
490        } else {
491            start.min(len) as usize
492        };
493
494        let end_idx = if args.len() > 2 {
495            let end = args[2]
496                .as_f64()
497                .map(|n| n as i64)
498                .ok_or_else(|| custom_error(ctx, "Expected number for end"))?;
499            if end < 0 {
500                (len + end).max(0) as usize
501            } else {
502                end.min(len) as usize
503            }
504        } else {
505            len as usize
506        };
507
508        let result: String = s
509            .chars()
510            .skip(start_idx)
511            .take(end_idx.saturating_sub(start_idx))
512            .collect();
513
514        Ok(Value::String(result))
515    }
516}
517
518// =============================================================================
519// concat(array_of_strings, separator?) -> string
520// =============================================================================
521
522defn!(ConcatFn, vec![arg!(array)], Some(arg!(string)));
523
524impl Function for ConcatFn {
525    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
526        self.signature.validate(args, ctx)?;
527        let arr = args[0]
528            .as_array()
529            .ok_or_else(|| custom_error(ctx, "Expected array argument"))?;
530
531        let separator = if args.len() > 1 {
532            args[1].as_str().map(|s| s.to_string()).unwrap_or_default()
533        } else {
534            String::new()
535        };
536
537        let strings: Vec<String> = arr
538            .iter()
539            .filter_map(|v| v.as_str().map(|s| s.to_string()))
540            .collect();
541
542        Ok(Value::String(strings.join(&separator)))
543    }
544}
545
546// =============================================================================
547// upper_case(string) -> string (alias for upper, snake_case style)
548// =============================================================================
549
550defn!(UpperCaseFn, vec![arg!(string)], None);
551
552impl Function for UpperCaseFn {
553    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
554        self.signature.validate(args, ctx)?;
555        let s = args[0]
556            .as_str()
557            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
558        Ok(Value::String(s.to_uppercase()))
559    }
560}
561
562// =============================================================================
563// lower_case(string) -> string (alias for lower, snake_case style)
564// =============================================================================
565
566defn!(LowerCaseFn, vec![arg!(string)], None);
567
568impl Function for LowerCaseFn {
569    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
570        self.signature.validate(args, ctx)?;
571        let s = args[0]
572            .as_str()
573            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
574        Ok(Value::String(s.to_lowercase()))
575    }
576}
577
578// =============================================================================
579// title_case(string) -> string (alias for title, snake_case style)
580// Uses heck crate for proper case conversion
581// =============================================================================
582
583defn!(TitleCaseFn, vec![arg!(string)], None);
584
585impl Function for TitleCaseFn {
586    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
587        self.signature.validate(args, ctx)?;
588        let s = args[0]
589            .as_str()
590            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
591        Ok(Value::String(s.to_title_case()))
592    }
593}
594
595// =============================================================================
596// camel_case(string) -> string (helloWorld)
597// Uses heck crate for proper case conversion
598// =============================================================================
599
600defn!(CamelCaseFn, vec![arg!(string)], None);
601
602impl Function for CamelCaseFn {
603    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
604        self.signature.validate(args, ctx)?;
605        let s = args[0]
606            .as_str()
607            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
608        Ok(Value::String(s.to_lower_camel_case()))
609    }
610}
611
612// =============================================================================
613// snake_case(string) -> string (hello_world)
614// Uses heck crate for proper case conversion
615// =============================================================================
616
617defn!(SnakeCaseFn, vec![arg!(string)], None);
618
619impl Function for SnakeCaseFn {
620    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
621        self.signature.validate(args, ctx)?;
622        let s = args[0]
623            .as_str()
624            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
625        Ok(Value::String(s.to_snake_case()))
626    }
627}
628
629// =============================================================================
630// kebab_case(string) -> string (hello-world)
631// Uses heck crate for proper case conversion
632// =============================================================================
633
634defn!(KebabCaseFn, vec![arg!(string)], None);
635
636impl Function for KebabCaseFn {
637    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
638        self.signature.validate(args, ctx)?;
639        let s = args[0]
640            .as_str()
641            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
642        Ok(Value::String(s.to_kebab_case()))
643    }
644}
645
646// =============================================================================
647// pascal_case(string) -> string (HelloWorld)
648// Uses heck crate - also known as UpperCamelCase
649// =============================================================================
650
651defn!(PascalCaseFn, vec![arg!(string)], None);
652
653impl Function for PascalCaseFn {
654    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
655        self.signature.validate(args, ctx)?;
656        let s = args[0]
657            .as_str()
658            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
659        Ok(Value::String(s.to_upper_camel_case()))
660    }
661}
662
663// =============================================================================
664// shouty_snake_case(string) -> string (HELLO_WORLD)
665// Uses heck crate - useful for constants
666// =============================================================================
667
668defn!(ShoutySnakeCaseFn, vec![arg!(string)], None);
669
670impl Function for ShoutySnakeCaseFn {
671    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
672        self.signature.validate(args, ctx)?;
673        let s = args[0]
674            .as_str()
675            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
676        Ok(Value::String(s.to_shouty_snake_case()))
677    }
678}
679
680// =============================================================================
681// shouty_kebab_case(string) -> string (HELLO-WORLD)
682// Uses heck crate
683// =============================================================================
684
685defn!(ShoutyKebabCaseFn, vec![arg!(string)], None);
686
687impl Function for ShoutyKebabCaseFn {
688    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
689        self.signature.validate(args, ctx)?;
690        let s = args[0]
691            .as_str()
692            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
693        Ok(Value::String(s.to_shouty_kebab_case()))
694    }
695}
696
697// =============================================================================
698// train_case(string) -> string (Hello-World)
699// Uses heck crate - like HTTP headers (Content-Type)
700// =============================================================================
701
702defn!(TrainCaseFn, vec![arg!(string)], None);
703
704impl Function for TrainCaseFn {
705    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
706        self.signature.validate(args, ctx)?;
707        let s = args[0]
708            .as_str()
709            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
710        Ok(Value::String(s.to_train_case()))
711    }
712}
713
714// =============================================================================
715// truncate(string, length, suffix?) -> string
716// =============================================================================
717
718defn!(
719    TruncateFn,
720    vec![arg!(string), arg!(number)],
721    Some(arg!(string))
722);
723
724impl Function for TruncateFn {
725    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
726        self.signature.validate(args, ctx)?;
727        let s = args[0]
728            .as_str()
729            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
730        let max_len = args[1]
731            .as_f64()
732            .ok_or_else(|| custom_error(ctx, "Expected number for length"))?
733            as usize;
734
735        let suffix = args
736            .get(2)
737            .and_then(|v| v.as_str())
738            .map(|s| s.to_string())
739            .unwrap_or_else(|| "...".to_string());
740
741        if s.len() <= max_len {
742            Ok(Value::String(s.to_string()))
743        } else {
744            let truncate_at = max_len.saturating_sub(suffix.len());
745            let truncated: String = s.chars().take(truncate_at).collect();
746            Ok(Value::String(format!("{}{}", truncated, suffix)))
747        }
748    }
749}
750
751// =============================================================================
752// wrap(string, width) -> string with newlines
753// =============================================================================
754
755defn!(WrapFn, vec![arg!(string), arg!(number)], None);
756
757impl Function for WrapFn {
758    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
759        self.signature.validate(args, ctx)?;
760        let s = args[0]
761            .as_str()
762            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
763        let width = args[1]
764            .as_f64()
765            .ok_or_else(|| custom_error(ctx, "Expected number for width"))?
766            as usize;
767
768        if width == 0 {
769            return Ok(Value::String(s.to_string()));
770        }
771
772        let mut lines: Vec<String> = Vec::new();
773
774        // Process each paragraph (separated by newlines) separately
775        for paragraph in s.split('\n') {
776            let mut current_line = String::new();
777
778            for word in paragraph.split_whitespace() {
779                if current_line.is_empty() {
780                    current_line = word.to_string();
781                } else if current_line.len() + 1 + word.len() <= width {
782                    current_line.push(' ');
783                    current_line.push_str(word);
784                } else {
785                    lines.push(current_line);
786                    current_line = word.to_string();
787                }
788            }
789
790            // Push the last line of this paragraph (even if empty to preserve blank lines)
791            lines.push(current_line);
792        }
793
794        // Remove trailing empty line if the input didn't end with a newline
795        if !s.ends_with('\n') && lines.last().is_some_and(|l| l.is_empty()) {
796            lines.pop();
797        }
798
799        // If we have no lines but had input, return the original
800        if lines.is_empty() && !s.is_empty() {
801            return Ok(Value::String(s.to_string()));
802        }
803
804        // Join lines with newlines and return as string
805        Ok(Value::String(lines.join("\n")))
806    }
807}
808
809// =============================================================================
810// format(template, args) -> string
811// Supports:
812//   - Positional with array: format('Hello {0}', ['World'])
813//   - Named with object: format('Hello {name}', {name: 'World'})
814//   - Variadic: format('Hello {0}', 'World')
815// =============================================================================
816
817defn!(FormatFn, vec![arg!(string)], Some(arg!(any)));
818
819/// Convert a Value to its string representation for formatting
820fn var_to_format_string(v: &Value) -> String {
821    match v {
822        Value::String(s) => s.clone(),
823        Value::Number(n) => n.to_string(),
824        Value::Bool(b) => b.to_string(),
825        Value::Null => "null".to_string(),
826        _ => serde_json::to_string(v).unwrap_or_else(|_| "null".to_string()),
827    }
828}
829
830impl Function for FormatFn {
831    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
832        self.signature.validate(args, ctx)?;
833        let template = args[0]
834            .as_str()
835            .ok_or_else(|| custom_error(ctx, "Expected template string"))?;
836
837        let mut result = template.to_string();
838
839        // Check if second arg is an array or object for unified formatting
840        if args.len() == 2 {
841            if let Some(arr) = args[1].as_array() {
842                // Array-based positional: format('Hello {0}', ['World'])
843                for (i, item) in arr.iter().enumerate() {
844                    let placeholder = format!("{{{}}}", i);
845                    let value = var_to_format_string(item);
846                    result = result.replace(&placeholder, &value);
847                }
848                return Ok(Value::String(result));
849            } else if let Some(obj) = args[1].as_object() {
850                // Object-based named: format('Hello {name}', {name: 'World'})
851                for (key, val) in obj.iter() {
852                    let placeholder = format!("{{{}}}", key);
853                    let value = var_to_format_string(val);
854                    result = result.replace(&placeholder, &value);
855                }
856                return Ok(Value::String(result));
857            }
858        }
859
860        // Fallback: variadic arguments format('Hello {0}', 'World')
861        for (i, arg) in args.iter().skip(1).enumerate() {
862            let placeholder = format!("{{{}}}", i);
863            let value = var_to_format_string(arg);
864            result = result.replace(&placeholder, &value);
865        }
866
867        Ok(Value::String(result))
868    }
869}
870
871// =============================================================================
872// sprintf(format_string, ...args) -> string
873// Printf-style formatting with %s, %d, %f, etc.
874// =============================================================================
875
876defn!(SprintfFn, vec![arg!(string)], Some(arg!(any)));
877
878impl Function for SprintfFn {
879    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
880        self.signature.validate(args, ctx)?;
881        let format_str = args[0]
882            .as_str()
883            .ok_or_else(|| custom_error(ctx, "Expected format string"))?;
884
885        // Get arguments - either from array or variadic
886        let format_args: Vec<&Value> = if args.len() == 2 {
887            if let Some(arr) = args[1].as_array() {
888                arr.iter().collect()
889            } else {
890                args.iter().skip(1).collect()
891            }
892        } else {
893            args.iter().skip(1).collect()
894        };
895
896        let mut result = String::new();
897        let mut arg_index = 0;
898        let mut chars = format_str.chars().peekable();
899
900        while let Some(c) = chars.next() {
901            if c == '%' {
902                if let Some(&next) = chars.peek() {
903                    if next == '%' {
904                        // Escaped %
905                        result.push('%');
906                        chars.next();
907                        continue;
908                    }
909
910                    // Parse format specifier
911                    let mut width = String::new();
912                    let mut precision = String::new();
913                    let mut in_precision = false;
914
915                    // Parse width and precision
916                    while let Some(&ch) = chars.peek() {
917                        if ch == '.' {
918                            in_precision = true;
919                            chars.next();
920                        } else if ch.is_ascii_digit() || ch == '-' || ch == '+' {
921                            if in_precision {
922                                precision.push(ch);
923                            } else {
924                                width.push(ch);
925                            }
926                            chars.next();
927                        } else {
928                            break;
929                        }
930                    }
931
932                    // Get the format type
933                    if let Some(fmt_type) = chars.next() {
934                        if arg_index < format_args.len() {
935                            let arg = format_args[arg_index];
936                            arg_index += 1;
937
938                            let formatted = match fmt_type {
939                                's' => var_to_format_string(arg),
940                                'd' | 'i' => {
941                                    if let Some(n) = arg.as_f64() {
942                                        format!("{}", n as i64)
943                                    } else {
944                                        "0".to_string()
945                                    }
946                                }
947                                'f' => {
948                                    if let Some(n) = arg.as_f64() {
949                                        let prec: usize = precision.parse().unwrap_or(6);
950                                        format!("{:.prec$}", n, prec = prec)
951                                    } else {
952                                        "0.0".to_string()
953                                    }
954                                }
955                                'e' => {
956                                    if let Some(n) = arg.as_f64() {
957                                        let prec: usize = precision.parse().unwrap_or(6);
958                                        format!("{:.prec$e}", n, prec = prec)
959                                    } else {
960                                        "0e0".to_string()
961                                    }
962                                }
963                                'x' => {
964                                    if let Some(n) = arg.as_f64() {
965                                        format!("{:x}", n as i64)
966                                    } else {
967                                        "0".to_string()
968                                    }
969                                }
970                                'X' => {
971                                    if let Some(n) = arg.as_f64() {
972                                        format!("{:X}", n as i64)
973                                    } else {
974                                        "0".to_string()
975                                    }
976                                }
977                                'o' => {
978                                    if let Some(n) = arg.as_f64() {
979                                        format!("{:o}", n as i64)
980                                    } else {
981                                        "0".to_string()
982                                    }
983                                }
984                                'b' => {
985                                    if let Some(n) = arg.as_f64() {
986                                        format!("{:b}", n as i64)
987                                    } else {
988                                        "0".to_string()
989                                    }
990                                }
991                                'c' => {
992                                    if let Some(n) = arg.as_f64() {
993                                        char::from_u32(n as u32)
994                                            .map(|c| c.to_string())
995                                            .unwrap_or_default()
996                                    } else if let Some(s) = arg.as_str() {
997                                        s.chars().next().map(|c| c.to_string()).unwrap_or_default()
998                                    } else {
999                                        String::new()
1000                                    }
1001                                }
1002                                _ => {
1003                                    // Unknown format, just output as-is
1004                                    format!("%{}{}", width, fmt_type)
1005                                }
1006                            };
1007
1008                            // Apply width if specified
1009                            if !width.is_empty() {
1010                                let w: i32 = width.parse().unwrap_or(0);
1011                                if w < 0 {
1012                                    // Left-align
1013                                    result.push_str(&format!(
1014                                        "{:<width$}",
1015                                        formatted,
1016                                        width = w.unsigned_abs() as usize
1017                                    ));
1018                                } else {
1019                                    // Right-align
1020                                    result.push_str(&format!(
1021                                        "{:>width$}",
1022                                        formatted,
1023                                        width = w as usize
1024                                    ));
1025                                }
1026                            } else {
1027                                result.push_str(&formatted);
1028                            }
1029                        } else {
1030                            // Not enough arguments, output placeholder
1031                            result.push('%');
1032                            result.push_str(&width);
1033                            if !precision.is_empty() {
1034                                result.push('.');
1035                                result.push_str(&precision);
1036                            }
1037                            result.push(fmt_type);
1038                        }
1039                    }
1040                } else {
1041                    // % at end of string
1042                    result.push('%');
1043                }
1044            } else {
1045                result.push(c);
1046            }
1047        }
1048
1049        Ok(Value::String(result))
1050    }
1051}
1052
1053// =============================================================================
1054// ltrimstr(string, prefix) -> string
1055// =============================================================================
1056
1057defn!(LtrimstrFn, vec![arg!(string), arg!(string)], None);
1058
1059impl Function for LtrimstrFn {
1060    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
1061        self.signature.validate(args, ctx)?;
1062        let s = args[0]
1063            .as_str()
1064            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
1065        let prefix = args[1]
1066            .as_str()
1067            .ok_or_else(|| custom_error(ctx, "Expected prefix string"))?;
1068
1069        let result = s.strip_prefix(prefix).unwrap_or(s).to_string();
1070        Ok(Value::String(result))
1071    }
1072}
1073
1074// =============================================================================
1075// rtrimstr(string, suffix) -> string
1076// =============================================================================
1077
1078defn!(RtrimstrFn, vec![arg!(string), arg!(string)], None);
1079
1080impl Function for RtrimstrFn {
1081    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
1082        self.signature.validate(args, ctx)?;
1083        let s = args[0]
1084            .as_str()
1085            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
1086        let suffix = args[1]
1087            .as_str()
1088            .ok_or_else(|| custom_error(ctx, "Expected suffix string"))?;
1089
1090        let result = s.strip_suffix(suffix).unwrap_or(s).to_string();
1091        Ok(Value::String(result))
1092    }
1093}
1094
1095// =============================================================================
1096// indices(string, search) -> array of indices
1097// =============================================================================
1098
1099defn!(IndicesFn, vec![arg!(string), arg!(string)], None);
1100
1101impl Function for IndicesFn {
1102    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
1103        self.signature.validate(args, ctx)?;
1104        let s = args[0]
1105            .as_str()
1106            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
1107        let search = args[1]
1108            .as_str()
1109            .ok_or_else(|| custom_error(ctx, "Expected search string"))?;
1110
1111        // Find all indices (including overlapping matches)
1112        let mut indices: Vec<Value> = Vec::new();
1113        if !search.is_empty() {
1114            let mut start = 0;
1115            while let Some(pos) = s[start..].find(search) {
1116                let actual_pos = start + pos;
1117                indices.push(Value::Number(Number::from(actual_pos as i64)));
1118                start = actual_pos + 1; // Move by 1 to find overlapping matches
1119            }
1120        }
1121
1122        Ok(Value::Array(indices))
1123    }
1124}
1125
1126// =============================================================================
1127// inside(search, string) -> boolean
1128// =============================================================================
1129
1130defn!(InsideFn, vec![arg!(string), arg!(string)], None);
1131
1132impl Function for InsideFn {
1133    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
1134        self.signature.validate(args, ctx)?;
1135        let search = args[0]
1136            .as_str()
1137            .ok_or_else(|| custom_error(ctx, "Expected search string"))?;
1138        let s = args[1]
1139            .as_str()
1140            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
1141
1142        Ok(Value::Bool(s.contains(search)))
1143    }
1144}
1145
1146// =============================================================================
1147// humanize(string) -> string
1148// Converts a camelCase, snake_case, or kebab-case string to a human-readable form
1149// =============================================================================
1150
1151defn!(HumanizeFn, vec![arg!(string)], None);
1152
1153impl Function for HumanizeFn {
1154    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
1155        self.signature.validate(args, ctx)?;
1156        let s = args[0]
1157            .as_str()
1158            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
1159
1160        // Split on underscores, hyphens, and camelCase boundaries
1161        let mut result = String::new();
1162        let mut prev_was_lower = false;
1163        let mut word_start = true;
1164
1165        for c in s.chars() {
1166            if c == '_' || c == '-' {
1167                if !result.is_empty() && !result.ends_with(' ') {
1168                    result.push(' ');
1169                }
1170                word_start = true;
1171                prev_was_lower = false;
1172            } else if c.is_uppercase() && prev_was_lower {
1173                // camelCase boundary
1174                result.push(' ');
1175                if word_start {
1176                    result.push(c); // Keep first letter of sentence uppercase
1177                } else {
1178                    result.push(c.to_lowercase().next().unwrap_or(c));
1179                }
1180                word_start = false;
1181                prev_was_lower = false;
1182            } else {
1183                if word_start && result.is_empty() {
1184                    // First character of the string - capitalize it
1185                    result.push(c.to_uppercase().next().unwrap_or(c));
1186                } else {
1187                    result.push(c.to_lowercase().next().unwrap_or(c));
1188                }
1189                prev_was_lower = c.is_lowercase();
1190                word_start = false;
1191            }
1192        }
1193
1194        Ok(Value::String(result))
1195    }
1196}
1197
1198// =============================================================================
1199// deburr(string) -> string
1200// Removes diacritical marks (accents) from characters
1201// =============================================================================
1202
1203defn!(DeburrrFn, vec![arg!(string)], None);
1204
1205impl Function for DeburrrFn {
1206    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
1207        self.signature.validate(args, ctx)?;
1208        let s = args[0]
1209            .as_str()
1210            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
1211
1212        // Remove diacritical marks by mapping accented characters to ASCII
1213        let result: String = s
1214            .chars()
1215            .map(|c| match c {
1216                '\u{00C0}' | '\u{00C1}' | '\u{00C2}' | '\u{00C3}' | '\u{00C4}' | '\u{00C5}' => 'A',
1217                '\u{00C6}' => 'A', // Could be "AE" but keeping single char
1218                '\u{00C7}' => 'C',
1219                '\u{00C8}' | '\u{00C9}' | '\u{00CA}' | '\u{00CB}' => 'E',
1220                '\u{00CC}' | '\u{00CD}' | '\u{00CE}' | '\u{00CF}' => 'I',
1221                '\u{00D0}' => 'D',
1222                '\u{00D1}' => 'N',
1223                '\u{00D2}' | '\u{00D3}' | '\u{00D4}' | '\u{00D5}' | '\u{00D6}' | '\u{00D8}' => 'O',
1224                '\u{00D9}' | '\u{00DA}' | '\u{00DB}' | '\u{00DC}' => 'U',
1225                '\u{00DD}' => 'Y',
1226                '\u{00DE}' => 'T', // Thorn
1227                '\u{00DF}' => 's', // German sharp s
1228                '\u{00E0}' | '\u{00E1}' | '\u{00E2}' | '\u{00E3}' | '\u{00E4}' | '\u{00E5}' => 'a',
1229                '\u{00E6}' => 'a', // Could be "ae" but keeping single char
1230                '\u{00E7}' => 'c',
1231                '\u{00E8}' | '\u{00E9}' | '\u{00EA}' | '\u{00EB}' => 'e',
1232                '\u{00EC}' | '\u{00ED}' | '\u{00EE}' | '\u{00EF}' => 'i',
1233                '\u{00F0}' => 'd',
1234                '\u{00F1}' => 'n',
1235                '\u{00F2}' | '\u{00F3}' | '\u{00F4}' | '\u{00F5}' | '\u{00F6}' | '\u{00F8}' => 'o',
1236                '\u{00F9}' | '\u{00FA}' | '\u{00FB}' | '\u{00FC}' => 'u',
1237                '\u{00FD}' | '\u{00FF}' => 'y',
1238                '\u{00FE}' => 't', // Thorn
1239                _ => c,
1240            })
1241            .collect();
1242
1243        Ok(Value::String(result))
1244    }
1245}
1246
1247// =============================================================================
1248// words(string) -> array
1249// Splits string into an array of words
1250// =============================================================================
1251
1252defn!(WordsFn, vec![arg!(string)], None);
1253
1254impl Function for WordsFn {
1255    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
1256        self.signature.validate(args, ctx)?;
1257        let s = args[0]
1258            .as_str()
1259            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
1260
1261        // Split on word boundaries: spaces, underscores, hyphens, and camelCase
1262        let mut words = Vec::new();
1263        let mut current_word = String::new();
1264        let mut prev_was_lower = false;
1265
1266        for c in s.chars() {
1267            if c.is_whitespace() || c == '_' || c == '-' {
1268                if !current_word.is_empty() {
1269                    words.push(Value::String(current_word.clone()));
1270                    current_word.clear();
1271                }
1272                prev_was_lower = false;
1273            } else if c.is_uppercase() && prev_was_lower {
1274                // camelCase boundary
1275                if !current_word.is_empty() {
1276                    words.push(Value::String(current_word.clone()));
1277                    current_word.clear();
1278                }
1279                current_word.push(c);
1280                prev_was_lower = false;
1281            } else {
1282                current_word.push(c);
1283                prev_was_lower = c.is_lowercase();
1284            }
1285        }
1286
1287        if !current_word.is_empty() {
1288            words.push(Value::String(current_word));
1289        }
1290
1291        Ok(Value::Array(words))
1292    }
1293}
1294
1295// =============================================================================
1296// escape(string) -> string
1297// Escapes HTML entities: &, <, >, ", '
1298// =============================================================================
1299
1300defn!(EscapeFn, vec![arg!(string)], None);
1301
1302impl Function for EscapeFn {
1303    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
1304        self.signature.validate(args, ctx)?;
1305        let s = args[0]
1306            .as_str()
1307            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
1308
1309        let result = s
1310            .replace('&', "&amp;")
1311            .replace('<', "&lt;")
1312            .replace('>', "&gt;")
1313            .replace('"', "&quot;")
1314            .replace('\'', "&#39;");
1315
1316        Ok(Value::String(result))
1317    }
1318}
1319
1320// =============================================================================
1321// unescape(string) -> string
1322// Unescapes HTML entities: &amp;, &lt;, &gt;, &quot;, &#39;
1323// =============================================================================
1324
1325defn!(UnescapeFn, vec![arg!(string)], None);
1326
1327impl Function for UnescapeFn {
1328    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
1329        self.signature.validate(args, ctx)?;
1330        let s = args[0]
1331            .as_str()
1332            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
1333
1334        let result = s
1335            .replace("&amp;", "&")
1336            .replace("&lt;", "<")
1337            .replace("&gt;", ">")
1338            .replace("&quot;", "\"")
1339            .replace("&#39;", "'");
1340
1341        Ok(Value::String(result))
1342    }
1343}
1344
1345// =============================================================================
1346// escape_regex(string) -> string
1347// Escapes special regex characters
1348// =============================================================================
1349
1350defn!(EscapeRegexFn, vec![arg!(string)], None);
1351
1352impl Function for EscapeRegexFn {
1353    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
1354        self.signature.validate(args, ctx)?;
1355        let s = args[0]
1356            .as_str()
1357            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
1358
1359        // Escape regex special characters: \ ^ $ . | ? * + ( ) [ ] { }
1360        let mut result = String::with_capacity(s.len() * 2);
1361        for c in s.chars() {
1362            match c {
1363                '\\' | '^' | '$' | '.' | '|' | '?' | '*' | '+' | '(' | ')' | '[' | ']' | '{'
1364                | '}' => {
1365                    result.push('\\');
1366                    result.push(c);
1367                }
1368                _ => result.push(c),
1369            }
1370        }
1371
1372        Ok(Value::String(result))
1373    }
1374}
1375
1376// =============================================================================
1377// start_case(string) -> string
1378// Converts string to Start Case (capitalize first letter of each word)
1379// =============================================================================
1380
1381defn!(StartCaseFn, vec![arg!(string)], None);
1382
1383impl Function for StartCaseFn {
1384    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
1385        self.signature.validate(args, ctx)?;
1386        let s = args[0]
1387            .as_str()
1388            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
1389
1390        // Split on word boundaries and capitalize each word
1391        let mut result = String::new();
1392        let mut prev_was_lower = false;
1393        let mut word_start = true;
1394
1395        for c in s.chars() {
1396            if c.is_whitespace() || c == '_' || c == '-' {
1397                if !result.is_empty() && !result.ends_with(' ') {
1398                    result.push(' ');
1399                }
1400                word_start = true;
1401                prev_was_lower = false;
1402            } else if c.is_uppercase() && prev_was_lower {
1403                // camelCase boundary - start new word
1404                result.push(' ');
1405                result.push(c); // Keep uppercase
1406                word_start = false;
1407                prev_was_lower = false;
1408            } else {
1409                if word_start {
1410                    result.push(c.to_uppercase().next().unwrap_or(c));
1411                } else {
1412                    result.push(c.to_lowercase().next().unwrap_or(c));
1413                }
1414                prev_was_lower = c.is_lowercase();
1415                word_start = false;
1416            }
1417        }
1418
1419        Ok(Value::String(result))
1420    }
1421}
1422
1423// =============================================================================
1424// mask(string, visible?, char?) -> string
1425// Mask a string, optionally keeping the last N characters visible
1426// =============================================================================
1427
1428defn!(MaskFn, vec![arg!(string)], Some(arg!(any)));
1429
1430impl Function for MaskFn {
1431    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
1432        self.signature.validate(args, ctx)?;
1433        let s = args[0]
1434            .as_str()
1435            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
1436
1437        // Number of characters to keep visible at the end (default: 0)
1438        let visible = if args.len() > 1 && !args[1].is_null() {
1439            args[1].as_f64().unwrap_or(0.0) as usize
1440        } else {
1441            0
1442        };
1443
1444        // Mask character (default: '*')
1445        let mask_char = if args.len() > 2 && !args[2].is_null() {
1446            args[2]
1447                .as_str()
1448                .and_then(|s| s.chars().next())
1449                .unwrap_or('*')
1450        } else {
1451            '*'
1452        };
1453
1454        let char_count = s.chars().count();
1455
1456        if visible >= char_count {
1457            // If visible >= length, return original string
1458            return Ok(Value::String(s.to_string()));
1459        }
1460
1461        let mask_count = char_count - visible;
1462        let masked: String = std::iter::repeat_n(mask_char, mask_count)
1463            .chain(s.chars().skip(mask_count))
1464            .collect();
1465
1466        Ok(Value::String(masked))
1467    }
1468}
1469
1470// =============================================================================
1471// redact(string, pattern, replacement?) -> string
1472// Replace all matches of a regex pattern with a replacement string
1473// =============================================================================
1474
1475defn!(RedactFn, vec![arg!(string), arg!(string)], Some(arg!(any)));
1476
1477impl Function for RedactFn {
1478    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
1479        self.signature.validate(args, ctx)?;
1480        let s = args[0]
1481            .as_str()
1482            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
1483        let pattern = args[1]
1484            .as_str()
1485            .ok_or_else(|| custom_error(ctx, "Expected string pattern"))?;
1486
1487        // Replacement string (default: "[REDACTED]")
1488        let replacement = if args.len() > 2 && !args[2].is_null() {
1489            args[2]
1490                .as_str()
1491                .map(|s| s.to_string())
1492                .unwrap_or_else(|| "[REDACTED]".to_string())
1493        } else {
1494            "[REDACTED]".to_string()
1495        };
1496
1497        let re = Regex::new(pattern)
1498            .map_err(|e| custom_error(ctx, &format!("Invalid regex pattern: {}", e)))?;
1499
1500        let result = re.replace_all(s, replacement.as_str());
1501        Ok(Value::String(result.into_owned()))
1502    }
1503}
1504
1505// =============================================================================
1506// normalize_whitespace(string) -> string
1507// Collapse multiple whitespace characters into a single space
1508// =============================================================================
1509
1510defn!(NormalizeWhitespaceFn, vec![arg!(string)], None);
1511
1512impl Function for NormalizeWhitespaceFn {
1513    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
1514        self.signature.validate(args, ctx)?;
1515        let s = args[0]
1516            .as_str()
1517            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
1518
1519        // Split on whitespace and rejoin with single spaces
1520        let result: String = s.split_whitespace().collect::<Vec<_>>().join(" ");
1521
1522        Ok(Value::String(result))
1523    }
1524}
1525
1526// =============================================================================
1527// is_blank(string) -> boolean
1528// Check if string is empty or contains only whitespace
1529// =============================================================================
1530
1531defn!(IsBlankFn, vec![arg!(string)], None);
1532
1533impl Function for IsBlankFn {
1534    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
1535        self.signature.validate(args, ctx)?;
1536        let s = args[0]
1537            .as_str()
1538            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
1539
1540        let is_blank = s.trim().is_empty();
1541        Ok(Value::Bool(is_blank))
1542    }
1543}
1544
1545// =============================================================================
1546// abbreviate(string, max_length, suffix?) -> string
1547// Truncate string to max length with ellipsis suffix
1548// =============================================================================
1549
1550defn!(
1551    AbbreviateFn,
1552    vec![arg!(string), arg!(number)],
1553    Some(arg!(any))
1554);
1555
1556impl Function for AbbreviateFn {
1557    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
1558        self.signature.validate(args, ctx)?;
1559        let s = args[0]
1560            .as_str()
1561            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
1562        let max_length = args[1]
1563            .as_f64()
1564            .ok_or_else(|| custom_error(ctx, "Expected number for max_length"))?
1565            as usize;
1566
1567        // Suffix (default: "...")
1568        let suffix = if args.len() > 2 && !args[2].is_null() {
1569            args[2]
1570                .as_str()
1571                .map(|s| s.to_string())
1572                .unwrap_or_else(|| "...".to_string())
1573        } else {
1574            "...".to_string()
1575        };
1576
1577        let char_count = s.chars().count();
1578        let suffix_len = suffix.chars().count();
1579
1580        if char_count <= max_length {
1581            return Ok(Value::String(s.to_string()));
1582        }
1583
1584        if max_length <= suffix_len {
1585            // If max_length is too small for suffix, just truncate
1586            let result: String = s.chars().take(max_length).collect();
1587            return Ok(Value::String(result));
1588        }
1589
1590        let truncate_at = max_length - suffix_len;
1591        let mut result: String = s.chars().take(truncate_at).collect();
1592        result.push_str(&suffix);
1593
1594        Ok(Value::String(result))
1595    }
1596}
1597
1598// =============================================================================
1599// center(string, width, char?) -> string
1600// Center-pad a string to the given width
1601// =============================================================================
1602
1603defn!(CenterFn, vec![arg!(string), arg!(number)], Some(arg!(any)));
1604
1605impl Function for CenterFn {
1606    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
1607        self.signature.validate(args, ctx)?;
1608        let s = args[0]
1609            .as_str()
1610            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
1611        let width = args[1]
1612            .as_f64()
1613            .ok_or_else(|| custom_error(ctx, "Expected number for width"))?
1614            as usize;
1615
1616        // Padding character (default: ' ')
1617        let pad_char = if args.len() > 2 && !args[2].is_null() {
1618            args[2]
1619                .as_str()
1620                .and_then(|s| s.chars().next())
1621                .unwrap_or(' ')
1622        } else {
1623            ' '
1624        };
1625
1626        let char_count = s.chars().count();
1627
1628        if char_count >= width {
1629            return Ok(Value::String(s.to_string()));
1630        }
1631
1632        let total_padding = width - char_count;
1633        let left_padding = total_padding / 2;
1634        let right_padding = total_padding - left_padding;
1635
1636        let mut result = String::with_capacity(width);
1637        for _ in 0..left_padding {
1638            result.push(pad_char);
1639        }
1640        result.push_str(s);
1641        for _ in 0..right_padding {
1642            result.push(pad_char);
1643        }
1644
1645        Ok(Value::String(result))
1646    }
1647}
1648
1649// =============================================================================
1650// reverse_string(string) -> string
1651// Reverse a string (Unicode-aware, reverses grapheme clusters ideally, chars for simplicity)
1652// =============================================================================
1653
1654defn!(ReverseStringFn, vec![arg!(string)], None);
1655
1656impl Function for ReverseStringFn {
1657    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
1658        self.signature.validate(args, ctx)?;
1659        let s = args[0]
1660            .as_str()
1661            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
1662
1663        let result: String = s.chars().rev().collect();
1664        Ok(Value::String(result))
1665    }
1666}
1667
1668// =============================================================================
1669// explode(string) -> array
1670// Convert a string to an array of Unicode codepoints (integers)
1671// =============================================================================
1672
1673defn!(ExplodeFn, vec![arg!(string)], None);
1674
1675impl Function for ExplodeFn {
1676    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
1677        self.signature.validate(args, ctx)?;
1678        let s = args[0]
1679            .as_str()
1680            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
1681
1682        let codepoints: Vec<Value> = s
1683            .chars()
1684            .map(|c| Value::Number(Number::from(c as u32)))
1685            .collect();
1686
1687        Ok(Value::Array(codepoints))
1688    }
1689}
1690
1691// =============================================================================
1692// implode(array) -> string
1693// Convert an array of Unicode codepoints (integers) back to a string
1694// =============================================================================
1695
1696defn!(ImplodeFn, vec![arg!(array)], None);
1697
1698impl Function for ImplodeFn {
1699    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
1700        self.signature.validate(args, ctx)?;
1701        let arr = args[0]
1702            .as_array()
1703            .ok_or_else(|| custom_error(ctx, "Expected array argument"))?;
1704
1705        let mut result = String::new();
1706        for item in arr.iter() {
1707            let codepoint = item
1708                .as_f64()
1709                .ok_or_else(|| custom_error(ctx, "Expected array of numbers (codepoints)"))?
1710                as u32;
1711
1712            let c = char::from_u32(codepoint).ok_or_else(|| {
1713                custom_error(ctx, &format!("Invalid Unicode codepoint: {}", codepoint))
1714            })?;
1715            result.push(c);
1716        }
1717
1718        Ok(Value::String(result))
1719    }
1720}
1721
1722// =============================================================================
1723// shell_escape(string) -> string
1724// Escape a string for safe use in shell commands (POSIX sh compatible)
1725// =============================================================================
1726
1727defn!(ShellEscapeFn, vec![arg!(string)], None);
1728
1729impl Function for ShellEscapeFn {
1730    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
1731        self.signature.validate(args, ctx)?;
1732        let s = args[0]
1733            .as_str()
1734            .ok_or_else(|| custom_error(ctx, "Expected string argument"))?;
1735
1736        // Use single quotes for shell escaping (POSIX compatible)
1737        // If the string contains single quotes, we need to handle them specially:
1738        // 'it'\''s' becomes: 'it' + \' + 's' (end quote, escaped quote, start quote)
1739        let escaped = if s.is_empty() {
1740            "''".to_string()
1741        } else if s
1742            .chars()
1743            .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.' || c == '/')
1744        {
1745            // Safe characters that don't need quoting
1746            s.to_string()
1747        } else if !s.contains('\'') {
1748            // No single quotes, just wrap in single quotes
1749            format!("'{}'", s)
1750        } else {
1751            // Contains single quotes - use the '\'' technique
1752            let mut result = String::with_capacity(s.len() + 10);
1753            result.push('\'');
1754            for c in s.chars() {
1755                if c == '\'' {
1756                    result.push_str("'\\''");
1757                } else {
1758                    result.push(c);
1759                }
1760            }
1761            result.push('\'');
1762            result
1763        };
1764
1765        Ok(Value::String(escaped))
1766    }
1767}
1768
1769// =============================================================================
1770// register_filtered
1771// =============================================================================
1772
1773/// Register only the string functions that are in the enabled set.
1774pub fn register_filtered(runtime: &mut Runtime, enabled: &HashSet<&str>) {
1775    register_if_enabled(runtime, "lower", enabled, Box::new(LowerFn::new()));
1776    register_if_enabled(runtime, "upper", enabled, Box::new(UpperFn::new()));
1777    register_if_enabled(runtime, "trim", enabled, Box::new(TrimFn::new()));
1778    register_if_enabled(runtime, "trim_left", enabled, Box::new(TrimStartFn::new()));
1779    register_if_enabled(runtime, "trim_right", enabled, Box::new(TrimEndFn::new()));
1780    register_if_enabled(runtime, "split", enabled, Box::new(SplitFn::new()));
1781    register_if_enabled(runtime, "replace", enabled, Box::new(ReplaceFn::new()));
1782    register_if_enabled(runtime, "pad_left", enabled, Box::new(PadLeftFn::new()));
1783    register_if_enabled(runtime, "pad_right", enabled, Box::new(PadRightFn::new()));
1784    register_if_enabled(runtime, "substr", enabled, Box::new(SubstrFn::new()));
1785    register_if_enabled(
1786        runtime,
1787        "capitalize",
1788        enabled,
1789        Box::new(CapitalizeFn::new()),
1790    );
1791    register_if_enabled(runtime, "title", enabled, Box::new(TitleFn::new()));
1792    register_if_enabled(runtime, "repeat", enabled, Box::new(RepeatFn::new()));
1793    register_if_enabled(runtime, "find_first", enabled, Box::new(IndexOfFn::new()));
1794    register_if_enabled(
1795        runtime,
1796        "find_last",
1797        enabled,
1798        Box::new(LastIndexOfFn::new()),
1799    );
1800    register_if_enabled(runtime, "slice", enabled, Box::new(SliceFn::new()));
1801    register_if_enabled(runtime, "concat", enabled, Box::new(ConcatFn::new()));
1802    register_if_enabled(runtime, "upper_case", enabled, Box::new(UpperCaseFn::new()));
1803    register_if_enabled(runtime, "lower_case", enabled, Box::new(LowerCaseFn::new()));
1804    register_if_enabled(runtime, "title_case", enabled, Box::new(TitleCaseFn::new()));
1805    register_if_enabled(runtime, "camel_case", enabled, Box::new(CamelCaseFn::new()));
1806    register_if_enabled(runtime, "snake_case", enabled, Box::new(SnakeCaseFn::new()));
1807    register_if_enabled(runtime, "kebab_case", enabled, Box::new(KebabCaseFn::new()));
1808    register_if_enabled(
1809        runtime,
1810        "pascal_case",
1811        enabled,
1812        Box::new(PascalCaseFn::new()),
1813    );
1814    register_if_enabled(
1815        runtime,
1816        "shouty_snake_case",
1817        enabled,
1818        Box::new(ShoutySnakeCaseFn::new()),
1819    );
1820    register_if_enabled(
1821        runtime,
1822        "shouty_kebab_case",
1823        enabled,
1824        Box::new(ShoutyKebabCaseFn::new()),
1825    );
1826    register_if_enabled(runtime, "train_case", enabled, Box::new(TrainCaseFn::new()));
1827    register_if_enabled(runtime, "truncate", enabled, Box::new(TruncateFn::new()));
1828    register_if_enabled(runtime, "wrap", enabled, Box::new(WrapFn::new()));
1829    register_if_enabled(runtime, "format", enabled, Box::new(FormatFn::new()));
1830    register_if_enabled(runtime, "sprintf", enabled, Box::new(SprintfFn::new()));
1831    register_if_enabled(runtime, "ltrimstr", enabled, Box::new(LtrimstrFn::new()));
1832    register_if_enabled(runtime, "rtrimstr", enabled, Box::new(RtrimstrFn::new()));
1833    register_if_enabled(runtime, "indices", enabled, Box::new(IndicesFn::new()));
1834    register_if_enabled(runtime, "inside", enabled, Box::new(InsideFn::new()));
1835    register_if_enabled(runtime, "humanize", enabled, Box::new(HumanizeFn::new()));
1836    register_if_enabled(runtime, "deburr", enabled, Box::new(DeburrrFn::new()));
1837    register_if_enabled(runtime, "words", enabled, Box::new(WordsFn::new()));
1838    register_if_enabled(runtime, "escape", enabled, Box::new(EscapeFn::new()));
1839    register_if_enabled(runtime, "unescape", enabled, Box::new(UnescapeFn::new()));
1840    register_if_enabled(
1841        runtime,
1842        "escape_regex",
1843        enabled,
1844        Box::new(EscapeRegexFn::new()),
1845    );
1846    register_if_enabled(runtime, "start_case", enabled, Box::new(StartCaseFn::new()));
1847    register_if_enabled(runtime, "mask", enabled, Box::new(MaskFn::new()));
1848    register_if_enabled(runtime, "redact", enabled, Box::new(RedactFn::new()));
1849    register_if_enabled(
1850        runtime,
1851        "normalize_whitespace",
1852        enabled,
1853        Box::new(NormalizeWhitespaceFn::new()),
1854    );
1855    register_if_enabled(runtime, "is_blank", enabled, Box::new(IsBlankFn::new()));
1856    register_if_enabled(
1857        runtime,
1858        "abbreviate",
1859        enabled,
1860        Box::new(AbbreviateFn::new()),
1861    );
1862    register_if_enabled(runtime, "center", enabled, Box::new(CenterFn::new()));
1863    register_if_enabled(
1864        runtime,
1865        "reverse_string",
1866        enabled,
1867        Box::new(ReverseStringFn::new()),
1868    );
1869    register_if_enabled(runtime, "explode", enabled, Box::new(ExplodeFn::new()));
1870    register_if_enabled(runtime, "implode", enabled, Box::new(ImplodeFn::new()));
1871    register_if_enabled(
1872        runtime,
1873        "shell_escape",
1874        enabled,
1875        Box::new(ShellEscapeFn::new()),
1876    );
1877}