Skip to main content

latexml_core/util/
text.rs

1/// kinds of delimiters used by the binding replacement strings
2pub enum Delimiter {
3  /// for ()
4  Parenthesis,
5  /// for {}
6  Brace,
7  /// for []
8  Bracket,
9}
10impl Delimiter {
11  fn open(&self) -> char {
12    use self::Delimiter::*;
13    match *self {
14      Parenthesis => '(',
15      Brace => '{',
16      Bracket => '[',
17    }
18  }
19  fn close(&self) -> char {
20    use self::Delimiter::*;
21    match *self {
22      Parenthesis => ')',
23      Brace => '}',
24      Bracket => ']',
25    }
26  }
27}
28
29/// Extract a bracketed subexpression at the start of a larger string. defaults to () delimiters.
30pub fn extract_bracketed(text: &mut String, delimiter: Option<&Delimiter>) -> Option<String> {
31  let open_delim = match delimiter {
32    None => '(',
33    Some(d) => d.open(),
34  };
35  let close_delim = match delimiter {
36    None => ')',
37    Some(d) => d.close(),
38  };
39  let mut has_open = false;
40  let mut has_close = false;
41
42  let mut extracted = String::new();
43  let mut level = 0;
44  while !text.is_empty() {
45    match text.remove(0) {
46      c if c == close_delim => {
47        has_close = true;
48        level -= 1;
49        if level < 1 {
50          break;
51        } else {
52          extracted.push(c);
53        }
54      },
55      c if c == open_delim => {
56        has_open = true;
57        // level up on open paren
58        level += 1;
59        if level > 1 {
60          extracted.push(c);
61        }
62      },
63      c => {
64        if level > 0 {
65          // if we are inside the parens, record the char
66          extracted.push(c)
67        } else if !c.is_whitespace() {
68          // whitespaces are neutral even outside of the delim blocks
69          // regular chars out of () body should terminate the expression
70          *text = c.to_string() + text;
71          break;
72        }
73      },
74    }
75  }
76
77  if has_open && has_close && level == 0 {
78    Some(extracted)
79  } else {
80    // signal malformed
81    None
82  }
83}
84
85// Nice tip from https://users.rust-lang.org/t/trim-string-in-place/15809/18
86pub fn trim_end_in_place(s: &mut String) {
87  let trimmed = s.trim_end();
88  s.truncate(trimmed.len());
89}
90
91// Nice tip from https://users.rust-lang.org/t/trim-string-in-place/15809/18
92pub fn trim_start_in_place(s: &mut String) {
93  let trimmed = s.trim_start();
94  s.replace_range(..(s.len() - trimmed.len()), "");
95}
96
97// Nice tip from https://users.rust-lang.org/t/trim-string-in-place/15809/18
98pub fn trim_in_place(s: &mut String) {
99  trim_end_in_place(s);
100  trim_start_in_place(s);
101}
102
103#[cfg(test)]
104mod tests {
105  use super::*;
106
107  #[test]
108  fn extract_bracketed_parens_default() {
109    let mut s = "(abc)rest".to_string();
110    let out = extract_bracketed(&mut s, None).expect("well-formed parens");
111    assert_eq!(out, "abc");
112    assert_eq!(s, "rest");
113  }
114
115  #[test]
116  fn extract_bracketed_braces() {
117    let mut s = "{foo}tail".to_string();
118    let out = extract_bracketed(&mut s, Some(&Delimiter::Brace)).expect("braces");
119    assert_eq!(out, "foo");
120    assert_eq!(s, "tail");
121  }
122
123  #[test]
124  fn extract_bracketed_brackets() {
125    let mut s = "[opt]body".to_string();
126    let out = extract_bracketed(&mut s, Some(&Delimiter::Bracket)).expect("brackets");
127    assert_eq!(out, "opt");
128    assert_eq!(s, "body");
129  }
130
131  #[test]
132  fn extract_bracketed_nested() {
133    let mut s = "(a(b)c)rest".to_string();
134    let out = extract_bracketed(&mut s, None).expect("nested parens");
135    assert_eq!(out, "a(b)c");
136    assert_eq!(s, "rest");
137  }
138
139  #[test]
140  fn extract_bracketed_malformed_returns_none() {
141    let mut s = "(unclosed".to_string();
142    let out = extract_bracketed(&mut s, None);
143    assert!(out.is_none(), "unclosed bracket returns None");
144  }
145
146  #[test]
147  fn trim_end_in_place_strips_trailing_space() {
148    let mut s = "foo   ".to_string();
149    trim_end_in_place(&mut s);
150    assert_eq!(s, "foo");
151  }
152
153  #[test]
154  fn trim_end_in_place_preserves_leading() {
155    let mut s = "  foo  ".to_string();
156    trim_end_in_place(&mut s);
157    assert_eq!(s, "  foo");
158  }
159
160  #[test]
161  fn trim_start_in_place_strips_leading_space() {
162    let mut s = "   foo".to_string();
163    trim_start_in_place(&mut s);
164    assert_eq!(s, "foo");
165  }
166
167  #[test]
168  fn trim_start_in_place_preserves_trailing() {
169    let mut s = "  foo  ".to_string();
170    trim_start_in_place(&mut s);
171    assert_eq!(s, "foo  ");
172  }
173
174  #[test]
175  fn trim_in_place_strips_both_ends() {
176    let mut s = "  foo bar  ".to_string();
177    trim_in_place(&mut s);
178    assert_eq!(s, "foo bar");
179  }
180
181  #[test]
182  fn trim_in_place_empty_input() {
183    let mut s = "   ".to_string();
184    trim_in_place(&mut s);
185    assert_eq!(s, "");
186  }
187
188  #[test]
189  fn trim_in_place_no_whitespace() {
190    let mut s = "already_clean".to_string();
191    trim_in_place(&mut s);
192    assert_eq!(s, "already_clean");
193  }
194}