Skip to main content

fancy_regex/
expand.rs

1use crate::parse::{parse_decimal, parse_id};
2use crate::{Captures, CompileError, Error, ParseError, Regex};
3use std::borrow::Cow;
4use std::io;
5use std::mem;
6
7/// A set of options for expanding a template string using the contents
8/// of capture groups.
9#[derive(Debug)]
10pub struct Expander {
11    sub_char: char,
12    open: &'static str,
13    close: &'static str,
14    allow_undelimited_name: bool,
15}
16
17impl Default for Expander {
18    /// Returns the default expander used by [`Captures::expand`].
19    ///
20    /// [`Captures::expand`]: struct.Captures.html#expand
21    fn default() -> Self {
22        Expander {
23            sub_char: '$',
24            open: "{",
25            close: "}",
26            allow_undelimited_name: true,
27        }
28    }
29}
30
31impl 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    pub fn python() -> Expander {
54        Expander {
55            sub_char: '\\',
56            open: "g<",
57            close: ">",
58            allow_undelimited_name: false,
59        }
60    }
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    pub fn check(&self, template: &str, regex: &Regex) -> crate::Result<()> {
70        let on_group_num = |num| {
71            if num == 0 {
72                Ok(())
73            } else if !regex.named_groups.is_empty() {
74                Err(Error::CompileError(CompileError::NamedBackrefOnly))
75            } else if num < regex.captures_len() {
76                Ok(())
77            } else {
78                Err(Error::CompileError(CompileError::InvalidBackref))
79            }
80        };
81        self.exec(template, |step| match step {
82            Step::Char(_) => Ok(()),
83            Step::GroupName(name) => {
84                if regex.named_groups.contains_key(name) {
85                    Ok(())
86                } else if let Ok(num) = name.parse() {
87                    on_group_num(num)
88                } else {
89                    Err(Error::CompileError(CompileError::InvalidBackref))
90                }
91            }
92            Step::GroupNum(num) => on_group_num(num),
93            Step::Error => Err(Error::ParseError(
94                0,
95                ParseError::GeneralParseError(
96                    "parse error in template while expanding".to_string(),
97                ),
98            )),
99        })
100    }
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    pub fn escape<'a>(&self, text: &'a str) -> Cow<'a, str> {
112        if text.contains(self.sub_char) {
113            let mut quoted = String::with_capacity(self.sub_char.len_utf8() * 2);
114            quoted.push(self.sub_char);
115            quoted.push(self.sub_char);
116            Cow::Owned(text.replace(self.sub_char, &quoted))
117        } else {
118            Cow::Borrowed(text)
119        }
120    }
121
122    #[doc(hidden)]
123    #[deprecated(since = "0.4.0", note = "Use `escape` instead.")]
124    pub fn quote<'a>(&self, text: &'a str) -> Cow<'a, str> {
125        self.escape(text)
126    }
127
128    /// Expands the template string `template` using the syntax defined
129    /// by this expander and the values of capture groups from `captures`.
130    pub fn expansion(&self, template: &str, captures: &Captures<'_>) -> String {
131        let mut cursor = io::Cursor::new(Vec::with_capacity(template.len()));
132        self.write_expansion(&mut cursor, template, captures)
133            .expect("expansion succeeded");
134        String::from_utf8(cursor.into_inner()).expect("expansion is UTF-8")
135    }
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    pub fn append_expansion(&self, dst: &mut String, template: &str, captures: &Captures<'_>) {
140        let pos = dst.len();
141        let mut cursor = io::Cursor::new(mem::replace(dst, String::new()).into_bytes());
142        cursor.set_position(pos as u64);
143        self.write_expansion(&mut cursor, template, captures)
144            .expect("expansion succeeded");
145        *dst = String::from_utf8(cursor.into_inner()).expect("expansion is UTF-8");
146    }
147
148    /// Writes the expansion produced by `expansion` to `dst`.  Potentially more efficient
149    /// than calling `expansion` directly and writing the result.
150    pub fn write_expansion(
151        &self,
152        mut dst: impl io::Write,
153        template: &str,
154        captures: &Captures<'_>,
155    ) -> io::Result<()> {
156        self.exec(template, |step| match step {
157            Step::Char(c) => write!(dst, "{}", c),
158            Step::GroupName(name) => {
159                if let Some(m) = captures.name(name) {
160                    write!(dst, "{}", m.as_str())
161                } else if let Some(m) = name.parse().ok().and_then(|num| captures.get(num)) {
162                    write!(dst, "{}", m.as_str())
163                } else {
164                    Ok(())
165                }
166            }
167            Step::GroupNum(num) => {
168                if let Some(m) = captures.get(num) {
169                    write!(dst, "{}", m.as_str())
170                } else {
171                    Ok(())
172                }
173            }
174            Step::Error => Ok(()),
175        })
176    }
177
178    fn exec<'t, E>(
179        &self,
180        template: &'t str,
181        mut f: impl FnMut(Step<'t>) -> Result<(), E>,
182    ) -> Result<(), E> {
183        debug_assert!(!self.open.is_empty());
184        debug_assert!(!self.close.is_empty());
185        let mut iter = template.chars();
186        while let Some(c) = iter.next() {
187            if c == self.sub_char {
188                let tail = iter.as_str();
189                let skip = if tail.starts_with(self.sub_char) {
190                    f(Step::Char(self.sub_char))?;
191                    1
192                } else if let Some((id, skip)) =
193                    parse_id(tail, self.open, self.close).or_else(|| {
194                        if self.allow_undelimited_name {
195                            parse_id(tail, "", "")
196                        } else {
197                            None
198                        }
199                    })
200                {
201                    f(Step::GroupName(id))?;
202                    skip
203                } else if let Some((skip, num)) = parse_decimal(tail, 0) {
204                    f(Step::GroupNum(num))?;
205                    skip
206                } else {
207                    f(Step::Error)?;
208                    f(Step::Char(self.sub_char))?;
209                    0
210                };
211                iter = iter.as_str()[skip..].chars();
212            } else {
213                f(Step::Char(c))?;
214            }
215        }
216        Ok(())
217    }
218}
219
220enum Step<'a> {
221    Char(char),
222    GroupName(&'a str),
223    GroupNum(usize),
224    Error,
225}