/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 | } |