]>
Commit | Line | Data |
---|---|---|
f20569fa XL |
1 | // Format string literals. |
2 | ||
3 | use regex::Regex; | |
4 | use unicode_categories::UnicodeCategories; | |
5 | use unicode_segmentation::UnicodeSegmentation; | |
6 | ||
7 | use crate::config::Config; | |
8 | use crate::shape::Shape; | |
9 | use crate::utils::{unicode_str_width, wrap_str}; | |
10 | ||
11 | const MIN_STRING: usize = 10; | |
12 | ||
13 | /// Describes the layout of a piece of text. | |
14 | pub(crate) struct StringFormat<'a> { | |
15 | /// The opening sequence of characters for the piece of text | |
16 | pub(crate) opener: &'a str, | |
17 | /// The closing sequence of characters for the piece of text | |
18 | pub(crate) closer: &'a str, | |
19 | /// The opening sequence of characters for a line | |
20 | pub(crate) line_start: &'a str, | |
21 | /// The closing sequence of characters for a line | |
22 | pub(crate) line_end: &'a str, | |
23 | /// The allocated box to fit the text into | |
24 | pub(crate) shape: Shape, | |
25 | /// Trim trailing whitespaces | |
26 | pub(crate) trim_end: bool, | |
27 | pub(crate) config: &'a Config, | |
28 | } | |
29 | ||
30 | impl<'a> StringFormat<'a> { | |
31 | pub(crate) fn new(shape: Shape, config: &'a Config) -> StringFormat<'a> { | |
32 | StringFormat { | |
33 | opener: "\"", | |
34 | closer: "\"", | |
35 | line_start: " ", | |
36 | line_end: "\\", | |
37 | shape, | |
38 | trim_end: false, | |
39 | config, | |
40 | } | |
41 | } | |
42 | ||
43 | /// Returns the maximum number of graphemes that is possible on a line while taking the | |
44 | /// indentation into account. | |
45 | /// | |
46 | /// If we cannot put at least a single character per line, the rewrite won't succeed. | |
47 | fn max_width_with_indent(&self) -> Option<usize> { | |
48 | Some( | |
49 | self.shape | |
50 | .width | |
51 | .checked_sub(self.opener.len() + self.line_end.len() + 1)? | |
52 | + 1, | |
53 | ) | |
54 | } | |
55 | ||
56 | /// Like max_width_with_indent but the indentation is not subtracted. | |
57 | /// This allows to fit more graphemes from the string on a line when | |
58 | /// SnippetState::EndWithLineFeed. | |
59 | fn max_width_without_indent(&self) -> Option<usize> { | |
94222f64 | 60 | self.config.max_width().checked_sub(self.line_end.len()) |
f20569fa XL |
61 | } |
62 | } | |
63 | ||
64 | pub(crate) fn rewrite_string<'a>( | |
65 | orig: &str, | |
66 | fmt: &StringFormat<'a>, | |
67 | newline_max_chars: usize, | |
68 | ) -> Option<String> { | |
69 | let max_width_with_indent = fmt.max_width_with_indent()?; | |
70 | let max_width_without_indent = fmt.max_width_without_indent()?; | |
71 | let indent_with_newline = fmt.shape.indent.to_string_with_newline(fmt.config); | |
72 | let indent_without_newline = fmt.shape.indent.to_string(fmt.config); | |
73 | ||
74 | // Strip line breaks. | |
75 | // With this regex applied, all remaining whitespaces are significant | |
76 | let strip_line_breaks_re = Regex::new(r"([^\\](\\\\)*)\\[\n\r][[:space:]]*").unwrap(); | |
77 | let stripped_str = strip_line_breaks_re.replace_all(orig, "$1"); | |
78 | ||
79 | let graphemes = UnicodeSegmentation::graphemes(&*stripped_str, false).collect::<Vec<&str>>(); | |
80 | ||
81 | // `cur_start` is the position in `orig` of the start of the current line. | |
82 | let mut cur_start = 0; | |
83 | let mut result = String::with_capacity( | |
84 | stripped_str | |
85 | .len() | |
86 | .checked_next_power_of_two() | |
87 | .unwrap_or(usize::max_value()), | |
88 | ); | |
89 | result.push_str(fmt.opener); | |
90 | ||
91 | // Snip a line at a time from `stripped_str` until it is used up. Push the snippet | |
92 | // onto result. | |
93 | let mut cur_max_width = max_width_with_indent; | |
94 | let is_bareline_ok = fmt.line_start.is_empty() || is_whitespace(fmt.line_start); | |
95 | loop { | |
96 | // All the input starting at cur_start fits on the current line | |
97 | if graphemes_width(&graphemes[cur_start..]) <= cur_max_width { | |
98 | for (i, grapheme) in graphemes[cur_start..].iter().enumerate() { | |
99 | if is_new_line(grapheme) { | |
100 | // take care of blank lines | |
101 | result = trim_end_but_line_feed(fmt.trim_end, result); | |
94222f64 | 102 | result.push('\n'); |
f20569fa XL |
103 | if !is_bareline_ok && cur_start + i + 1 < graphemes.len() { |
104 | result.push_str(&indent_without_newline); | |
105 | result.push_str(fmt.line_start); | |
106 | } | |
107 | } else { | |
108 | result.push_str(grapheme); | |
109 | } | |
110 | } | |
111 | result = trim_end_but_line_feed(fmt.trim_end, result); | |
112 | break; | |
113 | } | |
114 | ||
115 | // The input starting at cur_start needs to be broken | |
116 | match break_string( | |
117 | cur_max_width, | |
118 | fmt.trim_end, | |
119 | fmt.line_end, | |
120 | &graphemes[cur_start..], | |
121 | ) { | |
122 | SnippetState::LineEnd(line, len) => { | |
123 | result.push_str(&line); | |
124 | result.push_str(fmt.line_end); | |
125 | result.push_str(&indent_with_newline); | |
126 | result.push_str(fmt.line_start); | |
127 | cur_max_width = newline_max_chars; | |
128 | cur_start += len; | |
129 | } | |
130 | SnippetState::EndWithLineFeed(line, len) => { | |
131 | if line == "\n" && fmt.trim_end { | |
132 | result = result.trim_end().to_string(); | |
133 | } | |
134 | result.push_str(&line); | |
135 | if is_bareline_ok { | |
136 | // the next line can benefit from the full width | |
137 | cur_max_width = max_width_without_indent; | |
138 | } else { | |
139 | result.push_str(&indent_without_newline); | |
140 | result.push_str(fmt.line_start); | |
141 | cur_max_width = max_width_with_indent; | |
142 | } | |
143 | cur_start += len; | |
144 | } | |
145 | SnippetState::EndOfInput(line) => { | |
146 | result.push_str(&line); | |
147 | break; | |
148 | } | |
149 | } | |
150 | } | |
151 | ||
152 | result.push_str(fmt.closer); | |
153 | wrap_str(result, fmt.config.max_width(), fmt.shape) | |
154 | } | |
155 | ||
94222f64 | 156 | /// Returns the index to the end of the URL if the split at index of the given string includes a |
f20569fa XL |
157 | /// URL or alike. Otherwise, returns `None`. |
158 | fn detect_url(s: &[&str], index: usize) -> Option<usize> { | |
159 | let start = match s[..=index].iter().rposition(|g| is_whitespace(g)) { | |
160 | Some(pos) => pos + 1, | |
161 | None => 0, | |
162 | }; | |
163 | // 8 = minimum length for a string to contain a URL | |
164 | if s.len() < start + 8 { | |
165 | return None; | |
166 | } | |
167 | let split = s[start..].concat(); | |
168 | if split.contains("https://") | |
169 | || split.contains("http://") | |
170 | || split.contains("ftp://") | |
171 | || split.contains("file://") | |
172 | { | |
173 | match s[index..].iter().position(|g| is_whitespace(g)) { | |
174 | Some(pos) => Some(index + pos - 1), | |
175 | None => Some(s.len() - 1), | |
176 | } | |
177 | } else { | |
178 | None | |
179 | } | |
180 | } | |
181 | ||
182 | /// Trims whitespaces to the right except for the line feed character. | |
183 | fn trim_end_but_line_feed(trim_end: bool, result: String) -> String { | |
184 | let whitespace_except_line_feed = |c: char| c.is_whitespace() && c != '\n'; | |
185 | if trim_end && result.ends_with(whitespace_except_line_feed) { | |
186 | result | |
187 | .trim_end_matches(whitespace_except_line_feed) | |
188 | .to_string() | |
189 | } else { | |
190 | result | |
191 | } | |
192 | } | |
193 | ||
194 | /// Result of breaking a string so it fits in a line and the state it ended in. | |
195 | /// The state informs about what to do with the snippet and how to continue the breaking process. | |
196 | #[derive(Debug, PartialEq)] | |
197 | enum SnippetState { | |
198 | /// The input could not be broken and so rewriting the string is finished. | |
199 | EndOfInput(String), | |
200 | /// The input could be broken and the returned snippet should be ended with a | |
201 | /// `[StringFormat::line_end]`. The next snippet needs to be indented. | |
202 | /// | |
203 | /// The returned string is the line to print out and the number is the length that got read in | |
204 | /// the text being rewritten. That length may be greater than the returned string if trailing | |
205 | /// whitespaces got trimmed. | |
206 | LineEnd(String, usize), | |
207 | /// The input could be broken but a newline is present that cannot be trimmed. The next snippet | |
208 | /// to be rewritten *could* use more width than what is specified by the given shape. For | |
209 | /// example with a multiline string, the next snippet does not need to be indented, allowing | |
210 | /// more characters to be fit within a line. | |
211 | /// | |
212 | /// The returned string is the line to print out and the number is the length that got read in | |
213 | /// the text being rewritten. | |
214 | EndWithLineFeed(String, usize), | |
215 | } | |
216 | ||
217 | fn not_whitespace_except_line_feed(g: &str) -> bool { | |
218 | is_new_line(g) || !is_whitespace(g) | |
219 | } | |
220 | ||
221 | /// Break the input string at a boundary character around the offset `max_width`. A boundary | |
222 | /// character is either a punctuation or a whitespace. | |
223 | /// FIXME(issue#3281): We must follow UAX#14 algorithm instead of this. | |
224 | fn break_string(max_width: usize, trim_end: bool, line_end: &str, input: &[&str]) -> SnippetState { | |
225 | let break_at = |index /* grapheme at index is included */| { | |
226 | // Take in any whitespaces to the left/right of `input[index]` while | |
227 | // preserving line feeds | |
228 | let index_minus_ws = input[0..=index] | |
229 | .iter() | |
230 | .rposition(|grapheme| not_whitespace_except_line_feed(grapheme)) | |
231 | .unwrap_or(index); | |
232 | // Take into account newlines occurring in input[0..=index], i.e., the possible next new | |
233 | // line. If there is one, then text after it could be rewritten in a way that the available | |
234 | // space is fully used. | |
235 | for (i, grapheme) in input[0..=index].iter().enumerate() { | |
236 | if is_new_line(grapheme) { | |
237 | if i <= index_minus_ws { | |
238 | let mut line = &input[0..i].concat()[..]; | |
239 | if trim_end { | |
240 | line = line.trim_end(); | |
241 | } | |
242 | return SnippetState::EndWithLineFeed(format!("{}\n", line), i + 1); | |
243 | } | |
244 | break; | |
245 | } | |
246 | } | |
247 | ||
248 | let mut index_plus_ws = index; | |
249 | for (i, grapheme) in input[index + 1..].iter().enumerate() { | |
250 | if !trim_end && is_new_line(grapheme) { | |
251 | return SnippetState::EndWithLineFeed( | |
252 | input[0..=index + 1 + i].concat(), | |
253 | index + 2 + i, | |
254 | ); | |
255 | } else if not_whitespace_except_line_feed(grapheme) { | |
256 | index_plus_ws = index + i; | |
257 | break; | |
258 | } | |
259 | } | |
260 | ||
261 | if trim_end { | |
262 | SnippetState::LineEnd(input[0..=index_minus_ws].concat(), index_plus_ws + 1) | |
263 | } else { | |
264 | SnippetState::LineEnd(input[0..=index_plus_ws].concat(), index_plus_ws + 1) | |
265 | } | |
266 | }; | |
267 | ||
268 | // find a first index where the unicode width of input[0..x] become > max_width | |
269 | let max_width_index_in_input = { | |
270 | let mut cur_width = 0; | |
271 | let mut cur_index = 0; | |
272 | for (i, grapheme) in input.iter().enumerate() { | |
273 | cur_width += unicode_str_width(grapheme); | |
274 | cur_index = i; | |
275 | if cur_width > max_width { | |
276 | break; | |
277 | } | |
278 | } | |
279 | cur_index | |
280 | }; | |
5e7ed085 FG |
281 | if max_width_index_in_input == 0 { |
282 | return SnippetState::EndOfInput(input.concat()); | |
283 | } | |
f20569fa XL |
284 | |
285 | // Find the position in input for breaking the string | |
286 | if line_end.is_empty() | |
287 | && trim_end | |
288 | && !is_whitespace(input[max_width_index_in_input - 1]) | |
289 | && is_whitespace(input[max_width_index_in_input]) | |
290 | { | |
291 | // At a breaking point already | |
292 | // The line won't invalidate the rewriting because: | |
293 | // - no extra space needed for the line_end character | |
294 | // - extra whitespaces to the right can be trimmed | |
295 | return break_at(max_width_index_in_input - 1); | |
296 | } | |
297 | if let Some(url_index_end) = detect_url(input, max_width_index_in_input) { | |
298 | let index_plus_ws = url_index_end | |
299 | + input[url_index_end..] | |
300 | .iter() | |
301 | .skip(1) | |
302 | .position(|grapheme| not_whitespace_except_line_feed(grapheme)) | |
303 | .unwrap_or(0); | |
304 | return if trim_end { | |
305 | SnippetState::LineEnd(input[..=url_index_end].concat(), index_plus_ws + 1) | |
306 | } else { | |
5e7ed085 | 307 | SnippetState::LineEnd(input[..=index_plus_ws].concat(), index_plus_ws + 1) |
f20569fa XL |
308 | }; |
309 | } | |
310 | ||
311 | match input[0..max_width_index_in_input] | |
312 | .iter() | |
313 | .rposition(|grapheme| is_whitespace(grapheme)) | |
314 | { | |
315 | // Found a whitespace and what is on its left side is big enough. | |
316 | Some(index) if index >= MIN_STRING => break_at(index), | |
317 | // No whitespace found, try looking for a punctuation instead | |
318 | _ => match input[0..max_width_index_in_input] | |
319 | .iter() | |
320 | .rposition(|grapheme| is_punctuation(grapheme)) | |
321 | { | |
322 | // Found a punctuation and what is on its left side is big enough. | |
323 | Some(index) if index >= MIN_STRING => break_at(index), | |
324 | // Either no boundary character was found to the left of `input[max_chars]`, or the line | |
325 | // got too small. We try searching for a boundary character to the right. | |
326 | _ => match input[max_width_index_in_input..] | |
327 | .iter() | |
328 | .position(|grapheme| is_whitespace(grapheme) || is_punctuation(grapheme)) | |
329 | { | |
330 | // A boundary was found after the line limit | |
331 | Some(index) => break_at(max_width_index_in_input + index), | |
332 | // No boundary to the right, the input cannot be broken | |
333 | None => SnippetState::EndOfInput(input.concat()), | |
334 | }, | |
335 | }, | |
336 | } | |
337 | } | |
338 | ||
339 | fn is_new_line(grapheme: &str) -> bool { | |
340 | let bytes = grapheme.as_bytes(); | |
341 | bytes.starts_with(b"\n") || bytes.starts_with(b"\r\n") | |
342 | } | |
343 | ||
344 | fn is_whitespace(grapheme: &str) -> bool { | |
345 | grapheme.chars().all(char::is_whitespace) | |
346 | } | |
347 | ||
348 | fn is_punctuation(grapheme: &str) -> bool { | |
349 | grapheme | |
350 | .chars() | |
351 | .all(UnicodeCategories::is_punctuation_other) | |
352 | } | |
353 | ||
354 | fn graphemes_width(graphemes: &[&str]) -> usize { | |
355 | graphemes.iter().map(|s| unicode_str_width(s)).sum() | |
356 | } | |
357 | ||
358 | #[cfg(test)] | |
359 | mod test { | |
360 | use super::{break_string, detect_url, rewrite_string, SnippetState, StringFormat}; | |
361 | use crate::config::Config; | |
362 | use crate::shape::{Indent, Shape}; | |
363 | use unicode_segmentation::UnicodeSegmentation; | |
364 | ||
365 | #[test] | |
366 | fn issue343() { | |
367 | let config = Default::default(); | |
368 | let fmt = StringFormat::new(Shape::legacy(2, Indent::empty()), &config); | |
369 | rewrite_string("eq_", &fmt, 2); | |
370 | } | |
371 | ||
372 | #[test] | |
373 | fn should_break_on_whitespace() { | |
374 | let string = "Placerat felis. Mauris porta ante sagittis purus."; | |
375 | let graphemes = UnicodeSegmentation::graphemes(&*string, false).collect::<Vec<&str>>(); | |
376 | assert_eq!( | |
377 | break_string(20, false, "", &graphemes[..]), | |
378 | SnippetState::LineEnd("Placerat felis. ".to_string(), 16) | |
379 | ); | |
380 | assert_eq!( | |
381 | break_string(20, true, "", &graphemes[..]), | |
382 | SnippetState::LineEnd("Placerat felis.".to_string(), 16) | |
383 | ); | |
384 | } | |
385 | ||
386 | #[test] | |
387 | fn should_break_on_punctuation() { | |
388 | let string = "Placerat_felis._Mauris_porta_ante_sagittis_purus."; | |
389 | let graphemes = UnicodeSegmentation::graphemes(&*string, false).collect::<Vec<&str>>(); | |
390 | assert_eq!( | |
391 | break_string(20, false, "", &graphemes[..]), | |
392 | SnippetState::LineEnd("Placerat_felis.".to_string(), 15) | |
393 | ); | |
394 | } | |
395 | ||
396 | #[test] | |
397 | fn should_break_forward() { | |
398 | let string = "Venenatis_tellus_vel_tellus. Aliquam aliquam dolor at justo."; | |
399 | let graphemes = UnicodeSegmentation::graphemes(&*string, false).collect::<Vec<&str>>(); | |
400 | assert_eq!( | |
401 | break_string(20, false, "", &graphemes[..]), | |
402 | SnippetState::LineEnd("Venenatis_tellus_vel_tellus. ".to_string(), 29) | |
403 | ); | |
404 | assert_eq!( | |
405 | break_string(20, true, "", &graphemes[..]), | |
406 | SnippetState::LineEnd("Venenatis_tellus_vel_tellus.".to_string(), 29) | |
407 | ); | |
408 | } | |
409 | ||
410 | #[test] | |
411 | fn nothing_to_break() { | |
412 | let string = "Venenatis_tellus_vel_tellus"; | |
413 | let graphemes = UnicodeSegmentation::graphemes(&*string, false).collect::<Vec<&str>>(); | |
414 | assert_eq!( | |
415 | break_string(20, false, "", &graphemes[..]), | |
416 | SnippetState::EndOfInput("Venenatis_tellus_vel_tellus".to_string()) | |
417 | ); | |
418 | } | |
419 | ||
420 | #[test] | |
421 | fn significant_whitespaces() { | |
422 | let string = "Neque in sem. \n Pellentesque tellus augue."; | |
423 | let graphemes = UnicodeSegmentation::graphemes(&*string, false).collect::<Vec<&str>>(); | |
424 | assert_eq!( | |
425 | break_string(15, false, "", &graphemes[..]), | |
426 | SnippetState::EndWithLineFeed("Neque in sem. \n".to_string(), 20) | |
427 | ); | |
428 | assert_eq!( | |
429 | break_string(25, false, "", &graphemes[..]), | |
430 | SnippetState::EndWithLineFeed("Neque in sem. \n".to_string(), 20) | |
431 | ); | |
432 | ||
433 | assert_eq!( | |
434 | break_string(15, true, "", &graphemes[..]), | |
435 | SnippetState::LineEnd("Neque in sem.".to_string(), 19) | |
436 | ); | |
437 | assert_eq!( | |
438 | break_string(25, true, "", &graphemes[..]), | |
439 | SnippetState::EndWithLineFeed("Neque in sem.\n".to_string(), 20) | |
440 | ); | |
441 | } | |
442 | ||
443 | #[test] | |
444 | fn big_whitespace() { | |
445 | let string = "Neque in sem. Pellentesque tellus augue."; | |
446 | let graphemes = UnicodeSegmentation::graphemes(&*string, false).collect::<Vec<&str>>(); | |
447 | assert_eq!( | |
448 | break_string(20, false, "", &graphemes[..]), | |
449 | SnippetState::LineEnd("Neque in sem. ".to_string(), 25) | |
450 | ); | |
451 | assert_eq!( | |
452 | break_string(20, true, "", &graphemes[..]), | |
453 | SnippetState::LineEnd("Neque in sem.".to_string(), 25) | |
454 | ); | |
455 | } | |
456 | ||
457 | #[test] | |
458 | fn newline_in_candidate_line() { | |
459 | let string = "Nulla\nconsequat erat at massa. Vivamus id mi."; | |
460 | ||
461 | let graphemes = UnicodeSegmentation::graphemes(&*string, false).collect::<Vec<&str>>(); | |
462 | assert_eq!( | |
463 | break_string(25, false, "", &graphemes[..]), | |
464 | SnippetState::EndWithLineFeed("Nulla\n".to_string(), 6) | |
465 | ); | |
466 | assert_eq!( | |
467 | break_string(25, true, "", &graphemes[..]), | |
468 | SnippetState::EndWithLineFeed("Nulla\n".to_string(), 6) | |
469 | ); | |
470 | ||
471 | let mut config: Config = Default::default(); | |
472 | config.set().max_width(27); | |
473 | let fmt = StringFormat::new(Shape::legacy(25, Indent::empty()), &config); | |
474 | let rewritten_string = rewrite_string(string, &fmt, 27); | |
475 | assert_eq!( | |
476 | rewritten_string, | |
477 | Some("\"Nulla\nconsequat erat at massa. \\\n Vivamus id mi.\"".to_string()) | |
478 | ); | |
479 | } | |
480 | ||
481 | #[test] | |
482 | fn last_line_fit_with_trailing_whitespaces() { | |
483 | let string = "Vivamus id mi. "; | |
484 | let config: Config = Default::default(); | |
485 | let mut fmt = StringFormat::new(Shape::legacy(25, Indent::empty()), &config); | |
486 | ||
487 | fmt.trim_end = true; | |
488 | let rewritten_string = rewrite_string(string, &fmt, 25); | |
489 | assert_eq!(rewritten_string, Some("\"Vivamus id mi.\"".to_string())); | |
490 | ||
491 | fmt.trim_end = false; // default value of trim_end | |
492 | let rewritten_string = rewrite_string(string, &fmt, 25); | |
493 | assert_eq!(rewritten_string, Some("\"Vivamus id mi. \"".to_string())); | |
494 | } | |
495 | ||
496 | #[test] | |
497 | fn last_line_fit_with_newline() { | |
498 | let string = "Vivamus id mi.\nVivamus id mi."; | |
499 | let config: Config = Default::default(); | |
500 | let fmt = StringFormat { | |
501 | opener: "", | |
502 | closer: "", | |
503 | line_start: "// ", | |
504 | line_end: "", | |
505 | shape: Shape::legacy(100, Indent::from_width(&config, 4)), | |
506 | trim_end: true, | |
507 | config: &config, | |
508 | }; | |
509 | ||
510 | let rewritten_string = rewrite_string(string, &fmt, 100); | |
511 | assert_eq!( | |
512 | rewritten_string, | |
513 | Some("Vivamus id mi.\n // Vivamus id mi.".to_string()) | |
514 | ); | |
515 | } | |
516 | ||
517 | #[test] | |
518 | fn overflow_in_non_string_content() { | |
519 | let comment = "Aenean metus.\nVestibulum ac lacus. Vivamus porttitor"; | |
520 | let config: Config = Default::default(); | |
521 | let fmt = StringFormat { | |
522 | opener: "", | |
523 | closer: "", | |
524 | line_start: "// ", | |
525 | line_end: "", | |
526 | shape: Shape::legacy(30, Indent::from_width(&config, 8)), | |
527 | trim_end: true, | |
528 | config: &config, | |
529 | }; | |
530 | ||
531 | assert_eq!( | |
532 | rewrite_string(comment, &fmt, 30), | |
533 | Some( | |
534 | "Aenean metus.\n // Vestibulum ac lacus. Vivamus\n // porttitor" | |
535 | .to_string() | |
536 | ) | |
537 | ); | |
538 | } | |
539 | ||
540 | #[test] | |
541 | fn overflow_in_non_string_content_with_line_end() { | |
542 | let comment = "Aenean metus.\nVestibulum ac lacus. Vivamus porttitor"; | |
543 | let config: Config = Default::default(); | |
544 | let fmt = StringFormat { | |
545 | opener: "", | |
546 | closer: "", | |
547 | line_start: "// ", | |
548 | line_end: "@", | |
549 | shape: Shape::legacy(30, Indent::from_width(&config, 8)), | |
550 | trim_end: true, | |
551 | config: &config, | |
552 | }; | |
553 | ||
554 | assert_eq!( | |
555 | rewrite_string(comment, &fmt, 30), | |
556 | Some( | |
557 | "Aenean metus.\n // Vestibulum ac lacus. Vivamus@\n // porttitor" | |
558 | .to_string() | |
559 | ) | |
560 | ); | |
561 | } | |
562 | ||
563 | #[test] | |
564 | fn blank_line_with_non_empty_line_start() { | |
565 | let config: Config = Default::default(); | |
566 | let mut fmt = StringFormat { | |
567 | opener: "", | |
568 | closer: "", | |
569 | line_start: "// ", | |
570 | line_end: "", | |
571 | shape: Shape::legacy(30, Indent::from_width(&config, 4)), | |
572 | trim_end: true, | |
573 | config: &config, | |
574 | }; | |
575 | ||
576 | let comment = "Aenean metus. Vestibulum\n\nac lacus. Vivamus porttitor"; | |
577 | assert_eq!( | |
578 | rewrite_string(comment, &fmt, 30), | |
579 | Some( | |
580 | "Aenean metus. Vestibulum\n //\n // ac lacus. Vivamus porttitor".to_string() | |
581 | ) | |
582 | ); | |
583 | ||
584 | fmt.shape = Shape::legacy(15, Indent::from_width(&config, 4)); | |
585 | let comment = "Aenean\n\nmetus. Vestibulum ac lacus. Vivamus porttitor"; | |
586 | assert_eq!( | |
587 | rewrite_string(comment, &fmt, 15), | |
588 | Some( | |
589 | r#"Aenean | |
590 | // | |
591 | // metus. Vestibulum | |
592 | // ac lacus. Vivamus | |
593 | // porttitor"# | |
594 | .to_string() | |
595 | ) | |
596 | ); | |
597 | } | |
598 | ||
599 | #[test] | |
600 | fn retain_blank_lines() { | |
601 | let config: Config = Default::default(); | |
602 | let fmt = StringFormat { | |
603 | opener: "", | |
604 | closer: "", | |
605 | line_start: "// ", | |
606 | line_end: "", | |
607 | shape: Shape::legacy(20, Indent::from_width(&config, 4)), | |
608 | trim_end: true, | |
609 | config: &config, | |
610 | }; | |
611 | ||
612 | let comment = "Aenean\n\nmetus. Vestibulum ac lacus.\n\n"; | |
613 | assert_eq!( | |
614 | rewrite_string(comment, &fmt, 20), | |
615 | Some( | |
616 | "Aenean\n //\n // metus. Vestibulum ac\n // lacus.\n //\n".to_string() | |
617 | ) | |
618 | ); | |
619 | ||
620 | let comment = "Aenean\n\nmetus. Vestibulum ac lacus.\n"; | |
621 | assert_eq!( | |
622 | rewrite_string(comment, &fmt, 20), | |
623 | Some("Aenean\n //\n // metus. Vestibulum ac\n // lacus.\n".to_string()) | |
624 | ); | |
625 | ||
626 | let comment = "Aenean\n \nmetus. Vestibulum ac lacus."; | |
627 | assert_eq!( | |
628 | rewrite_string(comment, &fmt, 20), | |
629 | Some("Aenean\n //\n // metus. Vestibulum ac\n // lacus.".to_string()) | |
630 | ); | |
631 | } | |
632 | ||
633 | #[test] | |
634 | fn boundary_on_edge() { | |
635 | let config: Config = Default::default(); | |
636 | let mut fmt = StringFormat { | |
637 | opener: "", | |
638 | closer: "", | |
639 | line_start: "// ", | |
640 | line_end: "", | |
641 | shape: Shape::legacy(13, Indent::from_width(&config, 4)), | |
642 | trim_end: true, | |
643 | config: &config, | |
644 | }; | |
645 | ||
646 | let comment = "Aenean metus. Vestibulum ac lacus."; | |
647 | assert_eq!( | |
648 | rewrite_string(comment, &fmt, 13), | |
649 | Some("Aenean metus.\n // Vestibulum ac\n // lacus.".to_string()) | |
650 | ); | |
651 | ||
652 | fmt.trim_end = false; | |
653 | let comment = "Vestibulum ac lacus."; | |
654 | assert_eq!( | |
655 | rewrite_string(comment, &fmt, 13), | |
656 | Some("Vestibulum \n // ac lacus.".to_string()) | |
657 | ); | |
658 | ||
659 | fmt.trim_end = true; | |
660 | fmt.line_end = "\\"; | |
661 | let comment = "Vestibulum ac lacus."; | |
662 | assert_eq!( | |
663 | rewrite_string(comment, &fmt, 13), | |
664 | Some("Vestibulum\\\n // ac lacus.".to_string()) | |
665 | ); | |
666 | } | |
667 | ||
668 | #[test] | |
669 | fn detect_urls() { | |
670 | let string = "aaa http://example.org something"; | |
671 | let graphemes = UnicodeSegmentation::graphemes(&*string, false).collect::<Vec<&str>>(); | |
672 | assert_eq!(detect_url(&graphemes, 8), Some(21)); | |
673 | ||
674 | let string = "https://example.org something"; | |
675 | let graphemes = UnicodeSegmentation::graphemes(&*string, false).collect::<Vec<&str>>(); | |
676 | assert_eq!(detect_url(&graphemes, 0), Some(18)); | |
677 | ||
678 | let string = "aaa ftp://example.org something"; | |
679 | let graphemes = UnicodeSegmentation::graphemes(&*string, false).collect::<Vec<&str>>(); | |
680 | assert_eq!(detect_url(&graphemes, 8), Some(20)); | |
681 | ||
682 | let string = "aaa file://example.org something"; | |
683 | let graphemes = UnicodeSegmentation::graphemes(&*string, false).collect::<Vec<&str>>(); | |
684 | assert_eq!(detect_url(&graphemes, 8), Some(21)); | |
685 | ||
686 | let string = "aaa http not an url"; | |
687 | let graphemes = UnicodeSegmentation::graphemes(&*string, false).collect::<Vec<&str>>(); | |
688 | assert_eq!(detect_url(&graphemes, 6), None); | |
689 | ||
690 | let string = "aaa file://example.org"; | |
691 | let graphemes = UnicodeSegmentation::graphemes(&*string, false).collect::<Vec<&str>>(); | |
692 | assert_eq!(detect_url(&graphemes, 8), Some(21)); | |
693 | } | |
694 | } |