brk_string_wizard/magic_string/
trim.rs

1use std::borrow::Cow;
2use std::collections::VecDeque;
3
4use crate::MagicString;
5
6impl<'text> MagicString<'text> {
7  /// Trims whitespace from the start and end of the string.
8  pub fn trim(&mut self, char_type: Option<&str>) -> &mut Self {
9    self.trim_start(char_type);
10    self.trim_end(char_type);
11    self
12  }
13
14  /// Trims whitespace from the start of the string.
15  pub fn trim_start(&mut self, char_type: Option<&str>) -> &mut Self {
16    self.trim_start_aborted(char_type);
17    self
18  }
19
20  /// Trims whitespace from the end of the string.
21  pub fn trim_end(&mut self, char_type: Option<&str>) -> &mut Self {
22    self.trim_end_aborted(char_type);
23    self
24  }
25
26  /// Trims newlines from the start and end of the string.
27  pub fn trim_lines(&mut self) -> &mut Self {
28    self.trim(Some("[\r\n]"))
29  }
30
31  /// Internal method that trims from the start and returns true if aborted early.
32  fn trim_start_aborted(&mut self, char_type: Option<&str>) -> bool {
33    let pattern = char_type.unwrap_or("\\s");
34
35    // Trim intro
36    if trim_deque_start(&mut self.intro, pattern) {
37      return true;
38    }
39
40    // Trim chunks from start
41    let mut chunk_idx = Some(self.first_chunk_idx);
42    while let Some(idx) = chunk_idx {
43      let chunk = &self.chunks[idx];
44      let next_idx = chunk.next;
45
46      // Get the chunk content
47      let content = if let Some(ref edited) = chunk.edited_content {
48        edited.as_ref().to_string()
49      } else {
50        chunk.span.text(&self.source).to_string()
51      };
52
53      // Trim intro of chunk
54      let chunk = &mut self.chunks[idx];
55      if trim_deque_start(&mut chunk.intro, pattern) {
56        return true;
57      }
58
59      // Trim the content
60      let trimmed_content = trim_start_pattern(&content, pattern);
61
62      if content.is_empty() {
63        // Content is empty (e.g., from a remove), continue to next chunk
64        chunk_idx = next_idx;
65        continue;
66      }
67
68      if trimmed_content.len() == content.len() {
69        // No trimming happened and content is not empty, we're done
70        return true;
71      }
72
73      if !trimmed_content.is_empty() {
74        // Partial trim - update the chunk and return
75        chunk.edited_content = Some(trimmed_content.to_string().into());
76        return true;
77      }
78
79      // Entire content was trimmed - mark as empty
80      chunk.edited_content = Some("".into());
81
82      // Trim outro of this chunk - if non-whitespace remains, we're done
83      if trim_deque_start(&mut chunk.outro, pattern) {
84        return true;
85      }
86
87      chunk_idx = next_idx;
88    }
89
90    false
91  }
92
93  /// Internal method that trims from the end and returns true if aborted early.
94  fn trim_end_aborted(&mut self, char_type: Option<&str>) -> bool {
95    let pattern = char_type.unwrap_or("\\s");
96
97    // Trim outro
98    if trim_deque_end(&mut self.outro, pattern) {
99      return true;
100    }
101
102    // Trim chunks from end
103    let mut chunk_idx = Some(self.last_chunk_idx);
104    while let Some(idx) = chunk_idx {
105      let chunk = &self.chunks[idx];
106      let prev_idx = chunk.prev;
107
108      // Get the chunk content
109      let content = if let Some(ref edited) = chunk.edited_content {
110        edited.as_ref().to_string()
111      } else {
112        chunk.span.text(&self.source).to_string()
113      };
114
115      // Trim outro of chunk
116      let chunk = &mut self.chunks[idx];
117      if trim_deque_end(&mut chunk.outro, pattern) {
118        return true;
119      }
120
121      // Trim the content
122      let trimmed_content = trim_end_pattern(&content, pattern);
123
124      if content.is_empty() {
125        // Content is empty (e.g., from a remove), continue to prev chunk
126        chunk_idx = prev_idx;
127        continue;
128      }
129
130      if trimmed_content.len() == content.len() {
131        // No trimming happened and content is not empty, we're done
132        return true;
133      }
134
135      if !trimmed_content.is_empty() {
136        // Partial trim - update the chunk and return
137        chunk.edited_content = Some(trimmed_content.to_string().into());
138        return true;
139      }
140
141      // Entire content was trimmed - mark as empty
142      chunk.edited_content = Some("".into());
143
144      // Trim intro of this chunk - if non-whitespace remains, we're done
145      if trim_deque_end(&mut chunk.intro, pattern) {
146        return true;
147      }
148
149      chunk_idx = prev_idx;
150    }
151
152    false
153  }
154}
155
156/// Trims a deque from the start using the given pattern.
157/// Returns true if any non-empty content remains after trimming.
158fn trim_deque_start<'a>(deque: &mut VecDeque<Cow<'a, str>>, pattern: &str) -> bool {
159  let old_deque = std::mem::take(deque);
160  let mut found_non_match = false;
161
162  for s in old_deque {
163    if found_non_match {
164      deque.push_back(s);
165    } else {
166      let trimmed = trim_start_pattern(s.as_ref(), pattern);
167      if !trimmed.is_empty() {
168        deque.push_back(Cow::Owned(trimmed.to_string()));
169        found_non_match = true;
170      }
171    }
172  }
173
174  !deque.is_empty()
175}
176
177/// Trims a deque from the end using the given pattern.
178/// Returns true if any non-empty content remains after trimming.
179fn trim_deque_end<'a>(deque: &mut VecDeque<Cow<'a, str>>, pattern: &str) -> bool {
180  let old_deque = std::mem::take(deque);
181  let mut found_non_match = false;
182
183  for s in old_deque.into_iter().rev() {
184    if found_non_match {
185      deque.push_front(s);
186    } else {
187      let trimmed = trim_end_pattern(s.as_ref(), pattern);
188      if !trimmed.is_empty() {
189        deque.push_front(Cow::Owned(trimmed.to_string()));
190        found_non_match = true;
191      }
192    }
193  }
194
195  !deque.is_empty()
196}
197
198/// Trims characters matching the pattern from the start of the string.
199/// Supports common patterns and arbitrary regex patterns:
200/// - "\\s" -> whitespace
201/// - "[\\r\\n]" -> newlines only
202/// - Any other valid regex pattern
203fn trim_start_pattern<'a>(s: &'a str, pattern: &str) -> &'a str {
204  // Fast path for common patterns
205  match pattern {
206    "\\s" => return s.trim_start(),
207    "[\\r\\n]" | "[\r\n]" => return s.trim_start_matches(['\r', '\n']),
208    _ => {}
209  }
210
211  // Use regex for custom patterns
212  let regex_pattern = format!("^({pattern})+");
213  match regex::Regex::new(&regex_pattern) {
214    Ok(re) => {
215      if let Some(m) = re.find(s) {
216        &s[m.end()..]
217      } else {
218        s
219      }
220    }
221    Err(_) => s.trim_start(), // Fallback to whitespace on invalid regex
222  }
223}
224
225/// Trims characters matching the pattern from the end of the string.
226/// Supports common patterns and arbitrary regex patterns:
227/// - "\\s" -> whitespace
228/// - "[\\r\\n]" -> newlines only
229/// - Any other valid regex pattern
230fn trim_end_pattern<'a>(s: &'a str, pattern: &str) -> &'a str {
231  // Fast path for common patterns
232  match pattern {
233    "\\s" => return s.trim_end(),
234    "[\\r\\n]" | "[\r\n]" => return s.trim_end_matches(['\r', '\n']),
235    _ => {}
236  }
237
238  // Use regex for custom patterns
239  let regex_pattern = format!("({pattern})+$");
240  match regex::Regex::new(&regex_pattern) {
241    Ok(re) => {
242      if let Some(m) = re.find(s) {
243        &s[..m.start()]
244      } else {
245        s
246      }
247    }
248    Err(_) => s.trim_end(), // Fallback to whitespace on invalid regex
249  }
250}