Coverage Report

Created: 2025-06-02 07:01

/rust/registry/src/index.crates.io-6f17d22bba15001f/fancy-regex-0.11.0/src/expand.rs
Line
Count
Source (jump to first uncovered line)
1
use crate::parse::{parse_decimal, parse_id};
2
use crate::{Captures, CompileError, Error, ParseError, Regex};
3
use std::borrow::Cow;
4
use std::io;
5
use std::mem;
6
7
/// A set of options for expanding a template string using the contents
8
/// of capture groups.
9
#[derive(Debug)]
10
pub struct Expander {
11
    sub_char: char,
12
    open: &'static str,
13
    close: &'static str,
14
    allow_undelimited_name: bool,
15
}
16
17
impl Default for Expander {
18
    /// Returns the default expander used by [`Captures::expand`].
19
    ///
20
    /// [`Captures::expand`]: struct.Captures.html#expand
21
0
    fn default() -> Self {
22
0
        Expander {
23
0
            sub_char: '$',
24
0
            open: "{",
25
0
            close: "}",
26
0
            allow_undelimited_name: true,
27
0
        }
28
0
    }
29
}
30
31
impl Expander {
32
    /// Returns an expander that uses Python-compatible syntax.
33
    ///
34
    /// Expands all instances of `\num` or `\g<name>` in `replacement`
35
    /// to the corresponding capture group `num` or `name`, and writes
36
    /// them to the `dst` buffer given.
37
    ///
38
    /// `name` may be an integer corresponding to the index of the
39
    /// capture group (counted by order of opening parenthesis where `\0` is the
40
    /// entire match) or it can be a name (consisting of letters, digits or
41
    /// underscores) corresponding to a named capture group.
42
    ///
43
    /// `num` must be an integer corresponding to the index of the
44
    /// capture group.
45
    ///
46
    /// If `num` or `name` isn't a valid capture group (whether the name doesn't exist
47
    /// or isn't a valid index), then it is replaced with the empty string.
48
    ///
49
    /// The longest possible number is used. e.g., `\10` looks up capture
50
    /// group 10 and not capture group 1 followed by a literal 0.
51
    ///
52
    /// To write a literal `\`, use `\\`.
53
0
    pub fn python() -> Expander {
54
0
        Expander {
55
0
            sub_char: '\\',
56
0
            open: "g<",
57
0
            close: ">",
58
0
            allow_undelimited_name: false,
59
0
        }
60
0
    }
61
62
    /// Checks `template` for errors.  The following conditions are checked for:
63
    ///
64
    /// - A reference to a numbered group that does not exist in `regex`
65
    /// - A reference to a numbered group (other than 0) when `regex` contains named groups
66
    /// - A reference to a named group that does not occur in `regex`
67
    /// - An opening group name delimiter without a closing delimiter
68
    /// - Using an empty string as a group name
69
0
    pub fn check(&self, template: &str, regex: &Regex) -> crate::Result<()> {
70
0
        let on_group_num = |num| {
71
0
            if num == 0 {
72
0
                Ok(())
73
0
            } else if !regex.named_groups.is_empty() {
74
0
                Err(Error::CompileError(CompileError::NamedBackrefOnly))
75
0
            } else if num < regex.captures_len() {
76
0
                Ok(())
77
            } else {
78
0
                Err(Error::CompileError(CompileError::InvalidBackref))
79
            }
80
0
        };
81
0
        self.exec(template, |step| match step {
82
0
            Step::Char(_) => Ok(()),
83
0
            Step::GroupName(name) => {
84
0
                if regex.named_groups.contains_key(name) {
85
0
                    Ok(())
86
0
                } else if let Ok(num) = name.parse() {
87
0
                    on_group_num(num)
88
                } else {
89
0
                    Err(Error::CompileError(CompileError::InvalidBackref))
90
                }
91
            }
92
0
            Step::GroupNum(num) => on_group_num(num),
93
0
            Step::Error => Err(Error::ParseError(
94
0
                0,
95
0
                ParseError::GeneralParseError(
96
0
                    "parse error in template while expanding".to_string(),
97
0
                ),
98
0
            )),
99
0
        })
100
0
    }
101
102
    /// Escapes the substitution character in `text` so it appears literally
103
    /// in the output of `expansion`.
104
    ///
105
    /// ```
106
    /// assert_eq!(
107
    ///     fancy_regex::Expander::default().escape("Has a literal $ sign."),
108
    ///     "Has a literal $$ sign.",
109
    /// );
110
    /// ```
111
0
    pub fn escape<'a>(&self, text: &'a str) -> Cow<'a, str> {
112
0
        if text.contains(self.sub_char) {
113
0
            let mut quoted = String::with_capacity(self.sub_char.len_utf8() * 2);
114
0
            quoted.push(self.sub_char);
115
0
            quoted.push(self.sub_char);
116
0
            Cow::Owned(text.replace(self.sub_char, &quoted))
117
        } else {
118
0
            Cow::Borrowed(text)
119
        }
120
0
    }
121
122
    #[doc(hidden)]
123
    #[deprecated(since = "0.4.0", note = "Use `escape` instead.")]
124
0
    pub fn quote<'a>(&self, text: &'a str) -> Cow<'a, str> {
125
0
        self.escape(text)
126
0
    }
127
128
    /// Expands the template string `template` using the syntax defined
129
    /// by this expander and the values of capture groups from `captures`.
130
0
    pub fn expansion(&self, template: &str, captures: &Captures<'_>) -> String {
131
0
        let mut cursor = io::Cursor::new(Vec::with_capacity(template.len()));
132
0
        self.write_expansion(&mut cursor, template, captures)
133
0
            .expect("expansion succeeded");
134
0
        String::from_utf8(cursor.into_inner()).expect("expansion is UTF-8")
135
0
    }
136
137
    /// Appends the expansion produced by `expansion` to `dst`.  Potentially more efficient
138
    /// than calling `expansion` directly and appending to an existing string.
139
0
    pub fn append_expansion(&self, dst: &mut String, template: &str, captures: &Captures<'_>) {
140
0
        let pos = dst.len();
141
0
        let mut cursor = io::Cursor::new(mem::replace(dst, String::new()).into_bytes());
142
0
        cursor.set_position(pos as u64);
143
0
        self.write_expansion(&mut cursor, template, captures)
144
0
            .expect("expansion succeeded");
145
0
        *dst = String::from_utf8(cursor.into_inner()).expect("expansion is UTF-8");
146
0
    }
147
148
    /// Writes the expansion produced by `expansion` to `dst`.  Potentially more efficient
149
    /// than calling `expansion` directly and writing the result.
150
0
    pub fn write_expansion(
151
0
        &self,
152
0
        mut dst: impl io::Write,
153
0
        template: &str,
154
0
        captures: &Captures<'_>,
155
0
    ) -> io::Result<()> {
156
0
        self.exec(template, |step| match step {
157
0
            Step::Char(c) => write!(dst, "{}", c),
158
0
            Step::GroupName(name) => {
159
0
                if let Some(m) = captures.name(name) {
160
0
                    write!(dst, "{}", m.as_str())
161
0
                } else if let Some(m) = name.parse().ok().and_then(|num| captures.get(num)) {
162
0
                    write!(dst, "{}", m.as_str())
163
                } else {
164
0
                    Ok(())
165
                }
166
            }
167
0
            Step::GroupNum(num) => {
168
0
                if let Some(m) = captures.get(num) {
169
0
                    write!(dst, "{}", m.as_str())
170
                } else {
171
0
                    Ok(())
172
                }
173
            }
174
0
            Step::Error => Ok(()),
175
0
        })
176
0
    }
177
178
0
    fn exec<'t, E>(
179
0
        &self,
180
0
        template: &'t str,
181
0
        mut f: impl FnMut(Step<'t>) -> Result<(), E>,
182
0
    ) -> Result<(), E> {
183
0
        debug_assert!(!self.open.is_empty());
184
0
        debug_assert!(!self.close.is_empty());
185
0
        let mut iter = template.chars();
186
0
        while let Some(c) = iter.next() {
187
0
            if c == self.sub_char {
188
0
                let tail = iter.as_str();
189
0
                let skip = if tail.starts_with(self.sub_char) {
190
0
                    f(Step::Char(self.sub_char))?;
191
0
                    1
192
0
                } else if let Some((id, skip)) =
193
0
                    parse_id(tail, self.open, self.close).or_else(|| {
194
0
                        if self.allow_undelimited_name {
195
0
                            parse_id(tail, "", "")
196
                        } else {
197
0
                            None
198
                        }
199
0
                    })
Unexecuted instantiation: <fancy_regex::expand::Expander>::exec::<fancy_regex::error::Error, <fancy_regex::expand::Expander>::check::{closure#1}>::{closure#0}
Unexecuted instantiation: <fancy_regex::expand::Expander>::exec::<std::io::error::Error, <fancy_regex::expand::Expander>::write_expansion<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>::{closure#0}>::{closure#0}
200
                {
201
0
                    f(Step::GroupName(id))?;
202
0
                    skip
203
0
                } else if let Some((skip, num)) = parse_decimal(tail, 0) {
204
0
                    f(Step::GroupNum(num))?;
205
0
                    skip
206
                } else {
207
0
                    f(Step::Error)?;
208
0
                    f(Step::Char(self.sub_char))?;
209
0
                    0
210
                };
211
0
                iter = iter.as_str()[skip..].chars();
212
            } else {
213
0
                f(Step::Char(c))?;
214
            }
215
        }
216
0
        Ok(())
217
0
    }
Unexecuted instantiation: <fancy_regex::expand::Expander>::exec::<fancy_regex::error::Error, <fancy_regex::expand::Expander>::check::{closure#1}>
Unexecuted instantiation: <fancy_regex::expand::Expander>::exec::<std::io::error::Error, <fancy_regex::expand::Expander>::write_expansion<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>::{closure#0}>
218
}
219
220
enum Step<'a> {
221
    Char(char),
222
    GroupName(&'a str),
223
    GroupNum(usize),
224
    Error,
225
}