Coverage Report

Created: 2026-10-28 07:08

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/regress-0.12.0/src/api.rs
Line
Count
Source
1
use crate::classicalbacktrack;
2
use crate::emit;
3
use crate::exec;
4
use crate::indexing;
5
use crate::insn::CompiledRegex;
6
use crate::optimizer;
7
use crate::parse;
8
use crate::types::MAX_CAPTURE_GROUPS;
9
10
#[cfg(feature = "utf16")]
11
use crate::{
12
    classicalbacktrack::MatchAttempter,
13
    indexing::{InputIndexer, Ucs2Input, Utf16Input},
14
};
15
16
#[cfg(feature = "backend-pikevm")]
17
use crate::pikevm;
18
use crate::util::to_char_sat;
19
20
#[cfg(not(feature = "std"))]
21
use alloc::{
22
    boxed::Box,
23
    string::{String, ToString},
24
    vec::Vec,
25
};
26
use core::{fmt, iter::FusedIterator, str::FromStr};
27
28
pub use parse::Error;
29
30
/// Flags used to control regex parsing.
31
/// The default flags are case-sensitive, not-multiline, and optimizing.
32
#[derive(Debug, Copy, Clone, Default)]
33
pub struct Flags {
34
    /// If set, make the regex case-insensitive.
35
    /// Equivalent to the 'i' flag in JavaScript.
36
    pub icase: bool,
37
38
    /// If set, ^ and $ match at line separators, not just the input boundaries.
39
    /// Equivalent to the 'm' flag in JavaScript.
40
    pub multiline: bool,
41
42
    /// If set, . matches at line separators as well as any other character.
43
    /// Equivalent to the 's' flag in JavaScript.
44
    pub dot_all: bool,
45
46
    /// If set, disable regex IR passes.
47
    pub no_opt: bool,
48
49
    /// If set, the regex is interpreted as a Unicode regex.
50
    /// Equivalent to the 'u' flag in JavaScript.
51
    pub unicode: bool,
52
53
    /// If set, the regex is interpreted as a UnicodeSets regex.
54
    /// Equivalent to the 'v' flag in JavaScript.
55
    pub unicode_sets: bool,
56
}
57
58
impl Flags {
59
    /// Construct a Flags from a Unicode codepoints iterator, using JavaScript field names.
60
    /// 'i' means to ignore case, 'm' means multiline, 'u' means unicode.
61
    /// Note the 'g' flag implies a stateful regex and is not supported.
62
    /// Other flags are not implemented and are ignored.
63
    #[inline]
64
0
    pub fn new<T: Iterator<Item = u32>>(chars: T) -> Self {
65
0
        let mut result = Self::default();
66
0
        for c in chars {
67
0
            match to_char_sat(c) {
68
0
                'm' => {
69
0
                    result.multiline = true;
70
0
                }
71
0
                'i' => {
72
0
                    result.icase = true;
73
0
                }
74
0
                's' => {
75
0
                    result.dot_all = true;
76
0
                }
77
0
                'u' => {
78
0
                    result.unicode = true;
79
0
                }
80
0
                'v' => {
81
0
                    result.unicode_sets = true;
82
0
                }
83
0
                _ => {
84
0
                    // Silently skip unsupported flags.
85
0
                }
86
            }
87
        }
88
0
        result
89
0
    }
90
}
91
92
impl From<&str> for Flags {
93
    /// Construct a Flags from a string, using JavaScript field names.
94
    ///
95
    /// See also: [`Flags::new`].
96
    #[inline]
97
0
    fn from(s: &str) -> Self {
98
0
        Self::new(s.chars().map(u32::from))
99
0
    }
100
}
101
102
impl fmt::Display for Flags {
103
0
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
104
        // Destrucutre so we don't forget to print a field if new ones are added.
105
        let Self {
106
0
            icase,
107
0
            multiline,
108
0
            dot_all,
109
            no_opt: _,
110
0
            unicode,
111
0
            unicode_sets,
112
0
        } = *self;
113
0
        if multiline {
114
0
            f.write_str("m")?;
115
0
        }
116
0
        if icase {
117
0
            f.write_str("i")?;
118
0
        }
119
0
        if dot_all {
120
0
            f.write_str("s")?;
121
0
        }
122
0
        if unicode {
123
0
            f.write_str("u")?;
124
0
        }
125
0
        if unicode_sets {
126
0
            f.write_str("v")?;
127
0
        }
128
0
        Ok(())
129
0
    }
130
}
131
132
/// Range is used to express the extent of a match, as indexes into the input
133
/// string.
134
pub type Range = core::ops::Range<usize>;
135
136
/// An iterator type which yields `Match`es found in a string.
137
pub type Matches<'r, 't> = exec::Matches<backends::DefaultExecutor<'r, 't>>;
138
139
/// An iterator type which yields `Match`es found in a string, supporting ASCII
140
/// only.
141
pub type AsciiMatches<'r, 't> = exec::Matches<backends::DefaultAsciiExecutor<'r, 't>>;
142
143
/// A Match represents a portion of a string which was found to match a Regex.
144
#[derive(Debug, Clone)]
145
pub struct Match {
146
    /// The total range of the match. Note this may be empty, if the regex
147
    /// matched an empty string.
148
    pub range: Range,
149
150
    /// The list of captures. This has length equal to the number of capturing
151
    /// groups in the regex. For each capture, if the value is None, that group
152
    /// did not match (for example, it was in a not-taken branch of an
153
    /// alternation). If the value is Some, the group did match with the
154
    /// enclosed range.
155
    pub captures: Vec<Option<Range>>,
156
157
    // A list of capture group names. This is either:
158
    //   - Empty, if there were no named capture groups.
159
    //   - A list of names with length `captures.len()`, corresponding to the
160
    //     capture group names in order. Groups without names have an empty string.
161
    pub(crate) group_names: Box<[Box<str>]>,
162
}
163
164
impl Match {
165
    /// Access a group by index, using the convention of Python's group()
166
    /// function. Index 0 is the total match, index 1 is the first capture
167
    /// group.
168
    #[inline]
169
0
    pub fn group(&self, idx: usize) -> Option<Range> {
170
0
        if idx == 0 {
171
0
            Some(self.range.clone())
172
0
        } else if idx <= self.captures.len() {
173
0
            self.captures[idx - 1].clone()
174
        } else {
175
0
            None
176
        }
177
0
    }
178
179
    /// Access a named group by name.
180
    #[inline]
181
0
    pub fn named_group(&self, name: &str) -> Option<Range> {
182
        // Empty strings are used as sentinels to indicate unnamed group.
183
0
        if name.is_empty() {
184
0
            return None;
185
0
        }
186
0
        let pos = self.group_names.iter().position(|s| s.as_ref() == name)?;
187
0
        self.captures[pos].clone()
188
0
    }
189
190
    /// Return an iterator over the named groups of a Match.
191
    #[inline]
192
0
    pub fn named_groups(&self) -> NamedGroups<'_> {
193
0
        NamedGroups::new(self)
194
0
    }
195
196
    /// Returns the range over the starting and ending byte offsets of the match in the haystack.
197
    ///
198
    /// This is a convenience function to work around
199
    /// the fact that Range does not support Copy.
200
    #[inline]
201
0
    pub fn range(&self) -> Range {
202
0
        self.range.clone()
203
0
    }
Unexecuted instantiation: <regress::api::Match>::range
Unexecuted instantiation: <regress::api::Match>::range
204
205
    /// Returns the starting byte offset of the match in the haystack.
206
    #[inline]
207
0
    pub fn start(&self) -> usize {
208
0
        self.range.start
209
0
    }
210
211
    /// Returns the ending byte offset of the match in the haystack.
212
    #[inline]
213
0
    pub fn end(&self) -> usize {
214
0
        self.range.end
215
0
    }
216
217
    /// Returns the matched text as a string slice.
218
    ///
219
    /// # Examples
220
    ///
221
    /// ```rust
222
    /// use regress::Regex;
223
    ///
224
    /// let re = Regex::new(r"\d+").unwrap();
225
    /// let text = "Price: $123";
226
    /// let m = re.find(text).unwrap();
227
    /// assert_eq!(m.as_str(text), "123");
228
    /// ```
229
    #[inline]
230
0
    pub fn as_str<'t>(&self, text: &'t str) -> &'t str {
231
0
        &text[self.range()]
232
0
    }
233
234
    /// Return an iterator over a Match. The first returned value is the total
235
    /// match, and subsequent values represent the capture groups.
236
    #[inline]
237
0
    pub fn groups(&self) -> Groups<'_> {
238
0
        Groups::new(self)
239
0
    }
240
}
241
242
/// An iterator over the capture groups of a [`Match`]
243
///
244
/// This struct is created by the [`groups`] method on [`Match`].
245
///
246
/// [`Match`]: ../struct.Match.html
247
/// [`groups`]: ../struct.Match.html#method.groups
248
#[derive(Clone)]
249
pub struct Groups<'m> {
250
    mat: &'m Match,
251
252
    // The next group index to return, where 0 references the total match.
253
    next_group_idx: usize,
254
255
    // The maximum group index to return, with a +1 for the implicit total match.
256
    // For example, in a regex with 1 capture group, this will be 2.
257
    max: usize,
258
}
259
260
impl<'m> Groups<'m> {
261
    #[inline]
262
0
    fn new(mat: &'m Match) -> Self {
263
0
        Self {
264
0
            mat,
265
0
            next_group_idx: 0,
266
0
            max: mat.captures.len() + 1, // +1 for the total match
267
0
        }
268
0
    }
269
}
270
271
impl Iterator for Groups<'_> {
272
    type Item = Option<Range>;
273
274
    #[inline]
275
0
    fn next(&mut self) -> Option<Self::Item> {
276
0
        let i = self.next_group_idx;
277
0
        if i < self.max {
278
0
            self.next_group_idx += 1;
279
0
            Some(self.mat.group(i))
280
        } else {
281
0
            None
282
        }
283
0
    }
284
285
0
    fn size_hint(&self) -> (usize, Option<usize>) {
286
0
        let size = self.max.saturating_sub(self.next_group_idx);
287
0
        (size, Some(size))
288
0
    }
289
}
290
291
impl<'m> ExactSizeIterator for Groups<'m> {}
292
impl<'m> FusedIterator for Groups<'m> {}
293
294
/// An iterator over the named capture groups of a [`Match`]
295
///
296
/// This struct is created by the [`named_groups`] method on [`Match`].
297
///
298
/// [`Match`]: ../struct.Match.html
299
/// [`named_groups`]: ../struct.Match.html#method.named_groups
300
#[derive(Clone)]
301
pub struct NamedGroups<'m> {
302
    mat: &'m Match,
303
304
    // The next group name index to return.
305
    // Note unlike `Groups` this does NOT include the implicit total match.
306
    // That is, group 0 is the first capture group, NOT the total match.
307
    next_group_idx: usize,
308
}
309
310
impl<'m> NamedGroups<'m> {
311
    #[inline]
312
0
    fn new(mat: &'m Match) -> Self {
313
0
        Self {
314
0
            mat,
315
0
            next_group_idx: 0,
316
0
        }
317
0
    }
318
}
319
320
impl<'m> Iterator for NamedGroups<'m> {
321
    type Item = (&'m str, Option<Range>);
322
323
    #[inline]
324
0
    fn next(&mut self) -> Option<Self::Item> {
325
        // Increment next_group_idx until we find a non-empty name that we haven't seen yet.
326
0
        debug_assert!(self.next_group_idx <= self.mat.group_names.len());
327
0
        let end = self.mat.group_names.len();
328
329
        loop {
330
0
            let mut idx = self.next_group_idx;
331
            // Skip empty names
332
0
            while idx < end && self.mat.group_names[idx].is_empty() {
333
0
                idx += 1;
334
0
            }
335
0
            if idx == end {
336
0
                return None;
337
0
            }
338
339
0
            let name = self.mat.group_names[idx].as_ref();
340
341
            // Check if we've already returned this name (by looking backwards)
342
0
            let already_seen = self.mat.group_names[..idx]
343
0
                .iter()
344
0
                .any(|n| n.as_ref() == name);
345
346
0
            if already_seen {
347
                // Skip this duplicate and continue to next
348
0
                self.next_group_idx = idx + 1;
349
0
                continue;
350
0
            }
351
352
            // This is the first occurrence of this name. Find the best range value.
353
            // Prefer a Some value over None when there are duplicate names.
354
0
            let mut best_range = self.mat.captures[idx].clone();
355
0
            for check_idx in (idx + 1)..end {
356
0
                if self.mat.group_names[check_idx].as_ref() == name {
357
                    // Found a duplicate name. Prefer a Some value over None.
358
0
                    if best_range.is_none() && self.mat.captures[check_idx].is_some() {
359
0
                        best_range = self.mat.captures[check_idx].clone();
360
0
                        break; // Stop once we find a Some value
361
0
                    }
362
0
                }
363
            }
364
365
0
            self.next_group_idx = idx + 1;
366
0
            return Some((name, best_range));
367
        }
368
0
    }
369
370
0
    fn size_hint(&self) -> (usize, Option<usize>) {
371
0
        let size = self.mat.group_names[self.next_group_idx..]
372
0
            .iter()
373
0
            .filter(|s| !s.is_empty())
374
0
            .count();
375
376
0
        (size, Some(size))
377
0
    }
378
}
379
380
impl<'m> ExactSizeIterator for NamedGroups<'m> {}
381
impl<'m> FusedIterator for NamedGroups<'m> {}
382
383
/// A Regex is the compiled version of a pattern.
384
#[derive(Debug, Clone)]
385
pub struct Regex {
386
    cr: CompiledRegex,
387
}
388
389
impl From<CompiledRegex> for Regex {
390
0
    fn from(cr: CompiledRegex) -> Self {
391
0
        Self { cr }
392
0
    }
393
}
394
395
impl Regex {
396
    /// Construct a regex by parsing `pattern` using the default flags.
397
    /// An Error may be returned if the syntax is invalid.
398
    /// Note that this is rather expensive; prefer to cache a Regex which is
399
    /// intended to be used more than once.
400
0
    pub fn new(pattern: &str) -> Result<Regex, Error> {
401
0
        Self::with_flags(pattern, Flags::default())
402
0
    }
403
404
    /// Construct a regex by parsing `pattern` with `flags`.
405
    /// An Error may be returned if the syntax is invalid.
406
    //
407
    /// Note it is preferable to cache a Regex which is intended to be used more
408
    /// than once, as the parse may be expensive.
409
    #[inline]
410
0
    pub fn with_flags<F>(pattern: &str, flags: F) -> Result<Regex, Error>
411
0
    where
412
0
        F: Into<Flags>,
413
    {
414
0
        Self::from_unicode(pattern.chars().map(u32::from), flags)
415
0
    }
416
417
    /// Construct a regex by parsing `pattern` with `flags`, where
418
    /// `pattern` is an iterator of `u32` Unicode codepoints.
419
    /// An Error may be returned if the syntax is invalid.
420
    /// This allows parsing regular expressions from exotic strings in
421
    /// other encodings, such as UTF-16 or UTF-32.
422
0
    pub fn from_unicode<I, F>(pattern: I, flags: F) -> Result<Regex, Error>
423
0
    where
424
0
        I: Iterator<Item = u32> + Clone,
425
0
        F: Into<Flags>,
426
    {
427
0
        let flags = flags.into();
428
0
        let mut ire = parse::try_parse(pattern, flags)?;
429
0
        if !flags.no_opt {
430
0
            optimizer::optimize(&mut ire);
431
0
        }
432
0
        let cr = emit::emit(&ire);
433
0
        Ok(Regex { cr })
434
0
    }
435
436
    /// Searches `text` to find the first match.
437
0
    pub fn find(&self, text: &str) -> Option<Match> {
438
0
        self.find_iter(text).next()
439
0
    }
440
441
    /// Searches `text`, returning an iterator over non-overlapping matches.
442
    /// Note that the resulting Iterator borrows both the regex `'r` and the
443
    /// input string as `'t`.
444
    #[inline]
445
0
    pub fn find_iter<'r, 't>(&'r self, text: &'t str) -> Matches<'r, 't> {
446
0
        self.find_from(text, 0)
447
0
    }
448
449
    /// Returns an iterator for matches found in 'text' starting at byte index
450
    /// `start`. Note this may be different from passing a sliced `text` in
451
    /// the case of lookbehind assertions.
452
    /// Example:
453
    ///
454
    ///  ```rust
455
    ///   use regress::Regex;
456
    ///   let text = "xyxy";
457
    ///   let re = Regex::new(r"(?<=x)y").unwrap();
458
    ///   let t1 = re.find(&text[1..]).unwrap().range();
459
    ///   assert!(t1 == (2..3));
460
    ///   let t2 = re.find_from(text, 1).next().unwrap().range();
461
    ///   assert!(t2 == (1..2));
462
    ///   ```
463
    ///
464
    /// # Panics
465
    ///
466
    /// Panics if `start` is not on a UTF-8 character boundary of `text`. An
467
    /// out-of-range `start` (`start > text.len()`) is not an error and yields an
468
    /// empty iterator, mirroring JavaScript's `lastIndex` semantics.
469
    #[inline]
470
0
    pub fn find_from<'r, 't>(&'r self, text: &'t str, start: usize) -> Matches<'r, 't> {
471
0
        assert!(
472
0
            start >= text.len() || text.is_char_boundary(start),
473
            "start index is not on a char boundary"
474
        );
475
0
        backends::find(self, text, start)
476
0
    }
477
478
    /// Searches `text` to find the first match.
479
    /// The input text is expected to be ascii-only: only ASCII case-folding is
480
    /// supported.
481
0
    pub fn find_ascii(&self, text: &str) -> Option<Match> {
482
0
        self.find_iter_ascii(text).next()
483
0
    }
484
485
    /// Searches `text`, returning an iterator over non-overlapping matches.
486
    /// The input text is expected to be ascii-only: only ASCII case-folding is
487
    /// supported.
488
    #[inline]
489
0
    pub fn find_iter_ascii<'r, 't>(&'r self, text: &'t str) -> AsciiMatches<'r, 't> {
490
0
        self.find_from_ascii(text, 0)
491
0
    }
492
493
    /// Returns an iterator for matches found in 'text' starting at byte index
494
    /// `start`. An out-of-range `start` yields an empty iterator.
495
    #[inline]
496
0
    pub fn find_from_ascii<'r, 't>(&'r self, text: &'t str, start: usize) -> AsciiMatches<'r, 't> {
497
0
        backends::find(self, text, start)
498
0
    }
499
500
    /// Returns an iterator for matches found in 'text' starting at index `start`.
501
    #[cfg(feature = "utf16")]
502
    pub fn find_from_utf16<'r, 't>(
503
        &'r self,
504
        text: &'t [u16],
505
        start: usize,
506
    ) -> exec::Matches<super::classicalbacktrack::BacktrackExecutor<'r, indexing::Utf16Input<'t>>>
507
    {
508
        let input = Utf16Input::new(text, self.cr.flags.unicode);
509
        exec::Matches::new(
510
            super::classicalbacktrack::BacktrackExecutor::new(
511
                input,
512
                MatchAttempter::new(&self.cr, input.left_end()),
513
            ),
514
            start,
515
        )
516
    }
517
518
    /// Returns an iterator for matches found in 'text' starting at index `start`.
519
    #[cfg(feature = "utf16")]
520
    pub fn find_from_ucs2<'r, 't>(
521
        &'r self,
522
        text: &'t [u16],
523
        start: usize,
524
    ) -> exec::Matches<super::classicalbacktrack::BacktrackExecutor<'r, indexing::Ucs2Input<'t>>>
525
    {
526
        let input = Ucs2Input::new(text, self.cr.flags.unicode);
527
        exec::Matches::new(
528
            super::classicalbacktrack::BacktrackExecutor::new(
529
                input,
530
                MatchAttempter::new(&self.cr, input.left_end()),
531
            ),
532
            start,
533
        )
534
    }
535
536
    /// Replaces the first match of the regex in `text` with the replacement string.
537
    ///
538
    /// The replacement string may contain capture group references in the form `$1`, `$2`, etc.,
539
    /// where `$1` refers to the first capture group, `$2` to the second, and so on.
540
    /// `$0` refers to the entire match. Use `$$` to insert a literal `$`.
541
    ///
542
    /// If no match is found, the original text is returned unchanged.
543
    ///
544
    /// # Examples
545
    ///
546
    /// ```rust
547
    /// use regress::Regex;
548
    ///
549
    /// let re = Regex::new(r"(\w+)\s+(\w+)").unwrap();
550
    /// let result = re.replace("hello world", "$2 $1");
551
    /// assert_eq!(result, "world hello");
552
    ///
553
    /// let re = Regex::new(r"(\d{4})-(\d{2})-(\d{2})").unwrap();
554
    /// let result = re.replace("2023-12-25", "$2/$3/$1");
555
    /// assert_eq!(result, "12/25/2023");
556
    /// ```
557
0
    pub fn replace(&self, text: &str, replacement: &str) -> String {
558
0
        match self.find(text) {
559
0
            Some(m) => {
560
0
                let mut result = String::with_capacity(text.len());
561
0
                result.push_str(&text[..m.start()]);
562
0
                self.expand_replacement(&m, text, replacement, &mut result);
563
0
                result.push_str(&text[m.end()..]);
564
0
                result
565
            }
566
0
            None => text.to_string(),
567
        }
568
0
    }
569
570
    /// Replaces all matches of the regex in `text` with the replacement string.
571
    ///
572
    /// The replacement string may contain capture group references in the form `$1`, `$2`, etc.,
573
    /// where `$1` refers to the first capture group, `$2` to the second, and so on.
574
    /// `$0` refers to the entire match. Use `$$` to insert a literal `$`.
575
    ///
576
    /// # Examples
577
    ///
578
    /// ```rust
579
    /// use regress::Regex;
580
    ///
581
    /// let re = Regex::new(r"(\w+)\s+(\w+)").unwrap();
582
    /// let result = re.replace_all("hello world foo bar", "$2-$1");
583
    /// assert_eq!(result, "world-hello bar-foo");
584
    ///
585
    /// let re = Regex::new(r"\b(\w)(\w+)").unwrap();
586
    /// let result = re.replace_all("hello world", "$1.$2");
587
    /// assert_eq!(result, "h.ello w.orld");
588
    /// ```
589
0
    pub fn replace_all(&self, text: &str, replacement: &str) -> String {
590
0
        let mut result = String::with_capacity(text.len());
591
0
        let mut last_end = 0;
592
593
0
        for m in self.find_iter(text) {
594
0
            result.push_str(&text[last_end..m.start()]);
595
0
            self.expand_replacement(&m, text, replacement, &mut result);
596
0
            last_end = m.end();
597
0
        }
598
599
0
        result.push_str(&text[last_end..]);
600
0
        result
601
0
    }
602
603
    /// Replaces the first match of the regex in `text` using a closure.
604
    ///
605
    /// The closure receives a `&Match` and should return the replacement string.
606
    /// This is useful for dynamic replacements that depend on the match details.
607
    ///
608
    /// If no match is found, the original text is returned unchanged.
609
    ///
610
    /// # Examples
611
    ///
612
    /// ```rust
613
    /// use regress::Regex;
614
    ///
615
    /// let re = Regex::new(r"\d+").unwrap();
616
    /// let text = "Price: $123";
617
    /// let result = re.replace_with(text, |m| {
618
    ///     let num: i32 = m.as_str(text).parse().unwrap();
619
    ///     format!("{}", num * 2)
620
    /// });
621
    /// assert_eq!(result, "Price: $246");
622
    /// ```
623
0
    pub fn replace_with<F>(&self, text: &str, replacement: F) -> String
624
0
    where
625
0
        F: FnOnce(&Match) -> String,
626
    {
627
0
        match self.find(text) {
628
0
            Some(m) => {
629
0
                let mut result = String::with_capacity(text.len());
630
0
                result.push_str(&text[..m.start()]);
631
0
                result.push_str(&replacement(&m));
632
0
                result.push_str(&text[m.end()..]);
633
0
                result
634
            }
635
0
            None => text.to_string(),
636
        }
637
0
    }
638
639
    /// Replaces all matches of the regex in `text` using a closure.
640
    ///
641
    /// The closure receives a `&Match` and should return the replacement string.
642
    /// This is useful for dynamic replacements that depend on the match details.
643
    ///
644
    /// # Examples
645
    ///
646
    /// ```rust
647
    /// use regress::Regex;
648
    ///
649
    /// let re = Regex::new(r"\d+").unwrap();
650
    /// let text = "Items: 5, 10, 15";
651
    /// let result = re.replace_all_with(text, |m| {
652
    ///     let num: i32 = m.as_str(text).parse().unwrap();
653
    ///     format!("[{}]", num * 10)
654
    /// });
655
    /// assert_eq!(result, "Items: [50], [100], [150]");
656
    /// ```
657
0
    pub fn replace_all_with<F>(&self, text: &str, replacement: F) -> String
658
0
    where
659
0
        F: Fn(&Match) -> String,
660
    {
661
0
        let mut result = String::with_capacity(text.len());
662
0
        let mut last_end = 0;
663
664
0
        for m in self.find_iter(text) {
665
0
            result.push_str(&text[last_end..m.start()]);
666
0
            result.push_str(&replacement(&m));
667
0
            last_end = m.end();
668
0
        }
669
670
0
        result.push_str(&text[last_end..]);
671
0
        result
672
0
    }
673
674
    /// Helper method to expand replacement strings with capture group substitutions.
675
0
    fn expand_replacement(&self, m: &Match, text: &str, replacement: &str, output: &mut String) {
676
0
        let mut chars = replacement.chars().peekable();
677
678
0
        while let Some(ch) = chars.next() {
679
0
            if ch == '$' {
680
0
                match chars.peek() {
681
0
                    Some('$') => {
682
0
                        // $$ -> literal $
683
0
                        chars.next();
684
0
                        output.push('$');
685
0
                    }
686
0
                    Some(&digit) if digit.is_ascii_digit() => {
687
                        // Parse the group number
688
0
                        let mut group_num = 0;
689
0
                        while let Some(&digit) = chars.peek() {
690
0
                            if digit.is_ascii_digit() {
691
0
                                chars.next();
692
0
                                group_num = group_num * 10 + (digit as u32 - '0' as u32) as usize;
693
                                // Limit to reasonable group numbers to avoid overflow
694
0
                                if group_num > MAX_CAPTURE_GROUPS {
695
0
                                    break;
696
0
                                }
697
                            } else {
698
0
                                break;
699
                            }
700
                        }
701
702
                        // Get the matched text for this group
703
0
                        if let Some(range) = m.group(group_num) {
704
0
                            output.push_str(&text[range]);
705
0
                        }
706
                        // If group doesn't exist or didn't match, add nothing
707
                    }
708
                    Some('{') => {
709
                        // Handle ${name} syntax for named groups
710
0
                        chars.next(); // consume '{'
711
0
                        let mut name = String::new();
712
0
                        let mut found_closing_brace = false;
713
714
0
                        for ch in chars.by_ref() {
715
0
                            if ch == '}' {
716
0
                                found_closing_brace = true;
717
0
                                break;
718
0
                            }
719
0
                            name.push(ch);
720
                        }
721
722
0
                        if found_closing_brace {
723
0
                            if let Some(range) = m.named_group(&name) {
724
0
                                output.push_str(&text[range]);
725
0
                            }
726
0
                        } else {
727
0
                            // Malformed ${...}, treat as literal
728
0
                            output.push_str("${");
729
0
                            output.push_str(&name);
730
0
                        }
731
                    }
732
0
                    _ => {
733
0
                        // Just a $ at end or followed by non-digit
734
0
                        output.push('$');
735
0
                    }
736
                }
737
0
            } else {
738
0
                output.push(ch);
739
0
            }
740
        }
741
0
    }
742
}
743
744
impl FromStr for Regex {
745
    type Err = Error;
746
747
    /// Attempts to parse a string into a regular expression
748
    #[inline]
749
0
    fn from_str(s: &str) -> Result<Self, Error> {
750
0
        Self::new(s)
751
0
    }
752
}
753
754
// Pattern trait implementation for str::find, str::contains, etc.
755
#[cfg(feature = "pattern")]
756
mod pattern_impl {
757
    use super::*;
758
    use core::str::pattern::{Pattern, ReverseSearcher, SearchStep, Searcher};
759
760
    /// A searcher for a regex pattern.
761
    pub struct RegexSearcher<'r, 't> {
762
        haystack: &'t str,
763
        regex: &'r Regex,
764
        current_pos: usize,
765
        done: bool,
766
        // For reverse searching
767
        reverse_pos: usize,
768
        reverse_done: bool,
769
    }
770
771
    impl<'r, 't> RegexSearcher<'r, 't> {
772
        fn new(regex: &'r Regex, haystack: &'t str) -> Self {
773
            Self {
774
                haystack,
775
                regex,
776
                current_pos: 0,
777
                done: false,
778
                reverse_pos: haystack.len(),
779
                reverse_done: false,
780
            }
781
        }
782
783
        fn find_last_match_before(&self, pos: usize) -> Option<super::Match> {
784
            // Find all matches up to the given position and return the last one
785
            let mut last_match = None;
786
            for m in self.regex.find_from(self.haystack, 0) {
787
                if m.end() <= pos {
788
                    last_match = Some(m);
789
                } else {
790
                    break;
791
                }
792
            }
793
            last_match
794
        }
795
    }
796
797
    unsafe impl<'r, 't> Searcher<'t> for RegexSearcher<'r, 't> {
798
        fn haystack(&self) -> &'t str {
799
            self.haystack
800
        }
801
802
        fn next(&mut self) -> SearchStep {
803
            if self.done {
804
                return SearchStep::Done;
805
            }
806
807
            // Try to find the next match starting from current position
808
            if let Some(m) = self.regex.find_from(self.haystack, self.current_pos).next() {
809
                let match_start = m.start();
810
                let match_end = m.end();
811
812
                // Handle any gap between current position and match start
813
                if self.current_pos < match_start {
814
                    let reject_end = match_start;
815
                    let reject_start = self.current_pos;
816
                    self.current_pos = match_start;
817
                    return SearchStep::Reject(reject_start, reject_end);
818
                }
819
820
                // Return the match
821
                self.current_pos = match_end;
822
823
                // Handle zero-width matches to avoid infinite loops
824
                if match_start == match_end {
825
                    // For zero-width matches, we need to advance at least one byte
826
                    // to avoid infinite loops
827
                    if match_end < self.haystack.len() {
828
                        // Find the next character boundary
829
                        let mut next_pos = match_end + 1;
830
                        while next_pos < self.haystack.len()
831
                            && !self.haystack.is_char_boundary(next_pos)
832
                        {
833
                            next_pos += 1;
834
                        }
835
                        self.current_pos = next_pos;
836
                    } else {
837
                        // We're at the end of the string
838
                        self.done = true;
839
                    }
840
                }
841
842
                SearchStep::Match(match_start, match_end)
843
            } else {
844
                // No more matches, reject remaining text if any
845
                if self.current_pos < self.haystack.len() {
846
                    let reject_start = self.current_pos;
847
                    let reject_end = self.haystack.len();
848
                    self.current_pos = self.haystack.len();
849
                    self.done = true;
850
                    SearchStep::Reject(reject_start, reject_end)
851
                } else {
852
                    self.done = true;
853
                    SearchStep::Done
854
                }
855
            }
856
        }
857
    }
858
859
    unsafe impl<'r, 't> ReverseSearcher<'t> for RegexSearcher<'r, 't> {
860
        fn next_back(&mut self) -> SearchStep {
861
            if self.reverse_done {
862
                return SearchStep::Done;
863
            }
864
865
            // Try to find the last match before current reverse position
866
            if let Some(m) = self.find_last_match_before(self.reverse_pos) {
867
                let match_start = m.start();
868
                let match_end = m.end();
869
870
                // Handle any gap between match end and current reverse position
871
                if match_end < self.reverse_pos {
872
                    let reject_start = match_end;
873
                    let reject_end = self.reverse_pos;
874
                    self.reverse_pos = match_end;
875
                    return SearchStep::Reject(reject_start, reject_end);
876
                }
877
878
                // Return the match
879
                self.reverse_pos = match_start;
880
881
                // Handle zero-width matches
882
                if match_start == match_end {
883
                    // For zero-width matches, move back by one character
884
                    if match_start > 0 {
885
                        let mut prev_pos = match_start - 1;
886
                        while prev_pos > 0 && !self.haystack.is_char_boundary(prev_pos) {
887
                            prev_pos -= 1;
888
                        }
889
                        self.reverse_pos = prev_pos;
890
                    } else {
891
                        // We're at the beginning of the string
892
                        self.reverse_done = true;
893
                    }
894
                }
895
896
                SearchStep::Match(match_start, match_end)
897
            } else {
898
                // No more matches, reject remaining text if any
899
                if self.reverse_pos > 0 {
900
                    let reject_start = 0;
901
                    let reject_end = self.reverse_pos;
902
                    self.reverse_pos = 0;
903
                    self.reverse_done = true;
904
                    SearchStep::Reject(reject_start, reject_end)
905
                } else {
906
                    self.reverse_done = true;
907
                    SearchStep::Done
908
                }
909
            }
910
        }
911
    }
912
913
    impl<'r> Pattern for &'r Regex {
914
        type Searcher<'a> = RegexSearcher<'r, 'a>;
915
916
        fn into_searcher(self, haystack: &str) -> Self::Searcher<'_> {
917
            RegexSearcher::new(self, haystack)
918
        }
919
    }
920
}
921
922
#[cfg(feature = "pattern")]
923
pub use pattern_impl::*;
924
925
// Support for using regress with different regex backends.
926
// Currently there is only the classical backtracking, and PikeVM.
927
#[doc(hidden)]
928
pub mod backends {
929
    use super::Regex;
930
    use super::exec;
931
    use super::indexing;
932
    pub use crate::emit::emit;
933
    pub use crate::optimizer::optimize;
934
    pub use crate::parse::try_parse;
935
936
    /// An Executor using the classical backtracking algorithm.
937
    pub type BacktrackExecutor<'r, 't> =
938
        super::classicalbacktrack::BacktrackExecutor<'r, indexing::Utf8Input<'t>>;
939
940
    /// A Executor using the PikeVM executor.
941
    #[cfg(feature = "backend-pikevm")]
942
    pub type PikeVMExecutor<'r, 't> = super::pikevm::PikeVMExecutor<'r, indexing::Utf8Input<'t>>;
943
944
    /// An alias type to the default Executor.
945
    pub type DefaultExecutor<'r, 't> = BacktrackExecutor<'r, 't>;
946
947
    /// An alias type to the default executor's ASCII form.
948
    pub type DefaultAsciiExecutor<'r, 't> =
949
        <DefaultExecutor<'r, 't> as exec::Executor<'r, 't>>::AsAscii;
950
951
    /// Searches `text`, returning an iterator over non-overlapping matches.
952
0
    pub fn find<'r, 't, Executor: exec::Executor<'r, 't>>(
953
0
        re: &'r Regex,
954
0
        text: &'t str,
955
0
        start: usize,
956
0
    ) -> exec::Matches<Executor> {
957
0
        exec::Matches::new(Executor::new(&re.cr, text), start)
958
0
    }
Unexecuted instantiation: regress::api::backends::find::<regress::classicalbacktrack::BacktrackExecutor<regress::indexing::AsciiInput>>
Unexecuted instantiation: regress::api::backends::find::<regress::classicalbacktrack::BacktrackExecutor<regress::indexing::Utf8Input>>
959
960
    /// Searches `text`, returning an iterator over non-overlapping matches.
961
    /// This is a convenience method to avoid E0223.
962
0
    pub fn find_ascii<'r, 't, Executor: exec::Executor<'r, 't>>(
963
0
        re: &'r Regex,
964
0
        text: &'t str,
965
0
        start: usize,
966
0
    ) -> exec::Matches<Executor::AsAscii> {
967
0
        find::<Executor::AsAscii>(re, text, start)
968
0
    }
969
}
970
971
/// Escapes all special regex characters in a string to make it a literal match.
972
///
973
/// This function takes a string and returns a new string with all special
974
/// regex characters escaped with backslashes, so the resulting string can be
975
/// used as a literal pattern in a regular expression.
976
///
977
/// # Example
978
///
979
/// ```
980
/// use regress::escape;
981
///
982
/// let escaped = escape("Hello. How are you?");
983
/// assert_eq!(escaped, "Hello\\. How are you\\?");
984
///
985
/// let escaped = escape("$100 + tax (15%)");
986
/// assert_eq!(escaped, "\\$100 \\+ tax \\(15%\\)");
987
/// ```
988
0
pub fn escape(text: &str) -> String {
989
0
    let mut result = String::with_capacity(text.len());
990
991
0
    for c in text.chars() {
992
0
        match c {
993
            // Characters that have special meaning in regex and need escaping
994
0
            '\\' | '^' | '$' | '.' | '|' | '?' | '*' | '+' | '(' | ')' | '[' | ']' | '{' | '}' => {
995
0
                result.push('\\');
996
0
                result.push(c);
997
0
            }
998
            // All other characters are literal
999
0
            _ => result.push(c),
1000
        }
1001
    }
1002
1003
0
    result
1004
0
}