1 | use std::cell::Cell; |
2 | use std::convert::Infallible; |
3 | use std::fmt::{self, Write}; |
4 | use std::ops::Deref; |
5 | use std::pin::Pin; |
6 | |
7 | use super::escape::{FastWritable, HtmlSafeOutput}; |
8 | use crate::{Error, Result}; |
9 | |
10 | // MAX_LEN is maximum allowed length for filters. |
11 | const MAX_LEN: usize = 10_000; |
12 | |
13 | /// Formats arguments according to the specified format |
14 | /// |
15 | /// The *second* argument to this filter must be a string literal (as in normal |
16 | /// Rust). The two arguments are passed through to the `format!()` |
17 | /// [macro](https://doc.rust-lang.org/stable/std/macro.format.html) by |
18 | /// the Rinja code generator, but the order is swapped to support filter |
19 | /// composition. |
20 | /// |
21 | /// ```ignore |
22 | /// {{ value|fmt("{:?}" ) }} |
23 | /// ``` |
24 | /// |
25 | /// ``` |
26 | /// # #[cfg (feature = "code-in-doc" )] { |
27 | /// # use rinja::Template; |
28 | /// /// ```jinja |
29 | /// /// <div>{{ value|fmt("{:?}") }}</div> |
30 | /// /// ``` |
31 | /// #[derive(Template)] |
32 | /// #[template(ext = "html" , in_doc = true)] |
33 | /// struct Example { |
34 | /// value: (usize, usize), |
35 | /// } |
36 | /// |
37 | /// assert_eq!( |
38 | /// Example { value: (3, 4) }.to_string(), |
39 | /// "<div>(3, 4)</div>" |
40 | /// ); |
41 | /// # } |
42 | /// ``` |
43 | /// |
44 | /// Compare with [format](./fn.format.html). |
45 | pub fn fmt() {} |
46 | |
47 | /// Formats arguments according to the specified format |
48 | /// |
49 | /// The first argument to this filter must be a string literal (as in normal |
50 | /// Rust). All arguments are passed through to the `format!()` |
51 | /// [macro](https://doc.rust-lang.org/stable/std/macro.format.html) by |
52 | /// the Rinja code generator. |
53 | /// |
54 | /// ```ignore |
55 | /// {{ "{:?}{:?}" |format(value, other_value) }} |
56 | /// ``` |
57 | /// |
58 | /// ``` |
59 | /// # #[cfg (feature = "code-in-doc" )] { |
60 | /// # use rinja::Template; |
61 | /// /// ```jinja |
62 | /// /// <div>{{ "{:?}"|format(value) }}</div> |
63 | /// /// ``` |
64 | /// #[derive(Template)] |
65 | /// #[template(ext = "html" , in_doc = true)] |
66 | /// struct Example { |
67 | /// value: (usize, usize), |
68 | /// } |
69 | /// |
70 | /// assert_eq!( |
71 | /// Example { value: (3, 4) }.to_string(), |
72 | /// "<div>(3, 4)</div>" |
73 | /// ); |
74 | /// # } |
75 | /// ``` |
76 | /// |
77 | /// Compare with [fmt](./fn.fmt.html). |
78 | pub fn format() {} |
79 | |
80 | /// Replaces line breaks in plain text with appropriate HTML |
81 | /// |
82 | /// A single newline becomes an HTML line break `<br>` and a new line |
83 | /// followed by a blank line becomes a paragraph break `<p>`. |
84 | /// |
85 | /// ``` |
86 | /// # #[cfg (feature = "code-in-doc" )] { |
87 | /// # use rinja::Template; |
88 | /// /// ```jinja |
89 | /// /// <div>{{ example|linebreaks }}</div> |
90 | /// /// ``` |
91 | /// #[derive(Template)] |
92 | /// #[template(ext = "html" , in_doc = true)] |
93 | /// struct Example<'a> { |
94 | /// example: &'a str, |
95 | /// } |
96 | /// |
97 | /// assert_eq!( |
98 | /// Example { example: "Foo \nBar \n\nBaz" }.to_string(), |
99 | /// "<div><p>Foo<br/>Bar</p><p>Baz</p></div>" |
100 | /// ); |
101 | /// # } |
102 | /// ``` |
103 | #[inline ] |
104 | pub fn linebreaks(s: impl fmt::Display) -> Result<HtmlSafeOutput<String>, fmt::Error> { |
105 | fn linebreaks(s: String) -> String { |
106 | let linebroken: String = s.replace(" \n\n" , "</p><p>" ).replace(from:' \n' , to:"<br/>" ); |
107 | format!("<p> {linebroken}</p>" ) |
108 | } |
109 | Ok(HtmlSafeOutput(linebreaks(try_to_string(s)?))) |
110 | } |
111 | |
112 | /// Converts all newlines in a piece of plain text to HTML line breaks |
113 | /// |
114 | /// ``` |
115 | /// # #[cfg (feature = "code-in-doc" )] { |
116 | /// # use rinja::Template; |
117 | /// /// ```jinja |
118 | /// /// <div>{{ lines|linebreaksbr }}</div> |
119 | /// /// ``` |
120 | /// #[derive(Template)] |
121 | /// #[template(ext = "html" , in_doc = true)] |
122 | /// struct Example<'a> { |
123 | /// lines: &'a str, |
124 | /// } |
125 | /// |
126 | /// assert_eq!( |
127 | /// Example { lines: "a \nb \nc" }.to_string(), |
128 | /// "<div>a<br/>b<br/>c</div>" |
129 | /// ); |
130 | /// # } |
131 | /// ``` |
132 | #[inline ] |
133 | pub fn linebreaksbr(s: impl fmt::Display) -> Result<HtmlSafeOutput<String>, fmt::Error> { |
134 | fn linebreaksbr(s: String) -> String { |
135 | s.replace(from:' \n' , to:"<br/>" ) |
136 | } |
137 | Ok(HtmlSafeOutput(linebreaksbr(try_to_string(s)?))) |
138 | } |
139 | |
140 | /// Replaces only paragraph breaks in plain text with appropriate HTML |
141 | /// |
142 | /// A new line followed by a blank line becomes a paragraph break `<p>`. |
143 | /// Paragraph tags only wrap content; empty paragraphs are removed. |
144 | /// No `<br/>` tags are added. |
145 | /// |
146 | /// ``` |
147 | /// # #[cfg (feature = "code-in-doc" )] { |
148 | /// # use rinja::Template; |
149 | /// /// ```jinja |
150 | /// /// {{ lines|paragraphbreaks }} |
151 | /// /// ``` |
152 | /// #[derive(Template)] |
153 | /// #[template(ext = "html" , in_doc = true)] |
154 | /// struct Example<'a> { |
155 | /// lines: &'a str, |
156 | /// } |
157 | /// |
158 | /// assert_eq!( |
159 | /// Example { lines: "Foo \nBar \n\nBaz" }.to_string(), |
160 | /// "<p>Foo \nBar</p><p>Baz</p>" |
161 | /// ); |
162 | /// # } |
163 | /// ``` |
164 | #[inline ] |
165 | pub fn paragraphbreaks(s: impl fmt::Display) -> Result<HtmlSafeOutput<String>, fmt::Error> { |
166 | fn paragraphbreaks(s: String) -> String { |
167 | let linebroken: String = s.replace(" \n\n" , "</p><p>" ).replace(from:"<p></p>" , to:"" ); |
168 | format!("<p> {linebroken}</p>" ) |
169 | } |
170 | Ok(HtmlSafeOutput(paragraphbreaks(try_to_string(s)?))) |
171 | } |
172 | |
173 | /// Converts to lowercase |
174 | /// |
175 | /// ``` |
176 | /// # #[cfg (feature = "code-in-doc" )] { |
177 | /// # use rinja::Template; |
178 | /// /// ```jinja |
179 | /// /// <div>{{ word|lower }}</div> |
180 | /// /// ``` |
181 | /// #[derive(Template)] |
182 | /// #[template(ext = "html" , in_doc = true)] |
183 | /// struct Example<'a> { |
184 | /// word: &'a str, |
185 | /// } |
186 | /// |
187 | /// assert_eq!( |
188 | /// Example { word: "FOO" }.to_string(), |
189 | /// "<div>foo</div>" |
190 | /// ); |
191 | /// |
192 | /// assert_eq!( |
193 | /// Example { word: "FooBar" }.to_string(), |
194 | /// "<div>foobar</div>" |
195 | /// ); |
196 | /// # } |
197 | /// ``` |
198 | #[inline ] |
199 | pub fn lower(s: impl fmt::Display) -> Result<String, fmt::Error> { |
200 | fn lower(s: String) -> Result<String, fmt::Error> { |
201 | Ok(s.to_lowercase()) |
202 | } |
203 | lower(try_to_string(s)?) |
204 | } |
205 | |
206 | /// Converts to lowercase, alias for the `|lower` filter |
207 | /// |
208 | /// ``` |
209 | /// # #[cfg (feature = "code-in-doc" )] { |
210 | /// # use rinja::Template; |
211 | /// /// ```jinja |
212 | /// /// <div>{{ word|lowercase }}</div> |
213 | /// /// ``` |
214 | /// #[derive(Template)] |
215 | /// #[template(ext = "html" , in_doc = true)] |
216 | /// struct Example<'a> { |
217 | /// word: &'a str, |
218 | /// } |
219 | /// |
220 | /// assert_eq!( |
221 | /// Example { word: "FOO" }.to_string(), |
222 | /// "<div>foo</div>" |
223 | /// ); |
224 | /// |
225 | /// assert_eq!( |
226 | /// Example { word: "FooBar" }.to_string(), |
227 | /// "<div>foobar</div>" |
228 | /// ); |
229 | /// # } |
230 | /// ``` |
231 | #[inline ] |
232 | pub fn lowercase(s: impl fmt::Display) -> Result<String, fmt::Error> { |
233 | lower(s) |
234 | } |
235 | |
236 | /// Converts to uppercase |
237 | /// |
238 | /// ``` |
239 | /// # #[cfg (feature = "code-in-doc" )] { |
240 | /// # use rinja::Template; |
241 | /// /// ```jinja |
242 | /// /// <div>{{ word|upper }}</div> |
243 | /// /// ``` |
244 | /// #[derive(Template)] |
245 | /// #[template(ext = "html" , in_doc = true)] |
246 | /// struct Example<'a> { |
247 | /// word: &'a str, |
248 | /// } |
249 | /// |
250 | /// assert_eq!( |
251 | /// Example { word: "foo" }.to_string(), |
252 | /// "<div>FOO</div>" |
253 | /// ); |
254 | /// |
255 | /// assert_eq!( |
256 | /// Example { word: "FooBar" }.to_string(), |
257 | /// "<div>FOOBAR</div>" |
258 | /// ); |
259 | /// # } |
260 | /// ``` |
261 | #[inline ] |
262 | pub fn upper(s: impl fmt::Display) -> Result<String, fmt::Error> { |
263 | fn upper(s: String) -> Result<String, fmt::Error> { |
264 | Ok(s.to_uppercase()) |
265 | } |
266 | upper(try_to_string(s)?) |
267 | } |
268 | |
269 | /// Converts to uppercase, alias for the `|upper` filter |
270 | /// |
271 | /// ``` |
272 | /// # #[cfg (feature = "code-in-doc" )] { |
273 | /// # use rinja::Template; |
274 | /// /// ```jinja |
275 | /// /// <div>{{ word|uppercase }}</div> |
276 | /// /// ``` |
277 | /// #[derive(Template)] |
278 | /// #[template(ext = "html" , in_doc = true)] |
279 | /// struct Example<'a> { |
280 | /// word: &'a str, |
281 | /// } |
282 | /// |
283 | /// assert_eq!( |
284 | /// Example { word: "foo" }.to_string(), |
285 | /// "<div>FOO</div>" |
286 | /// ); |
287 | /// |
288 | /// assert_eq!( |
289 | /// Example { word: "FooBar" }.to_string(), |
290 | /// "<div>FOOBAR</div>" |
291 | /// ); |
292 | /// # } |
293 | /// ``` |
294 | #[inline ] |
295 | pub fn uppercase(s: impl fmt::Display) -> Result<String, fmt::Error> { |
296 | upper(s) |
297 | } |
298 | |
299 | /// Strip leading and trailing whitespace |
300 | /// |
301 | /// ``` |
302 | /// # #[cfg (feature = "code-in-doc" )] { |
303 | /// # use rinja::Template; |
304 | /// /// ```jinja |
305 | /// /// <div>{{ example|trim }}</div> |
306 | /// /// ``` |
307 | /// #[derive(Template)] |
308 | /// #[template(ext = "html" , in_doc = true)] |
309 | /// struct Example<'a> { |
310 | /// example: &'a str, |
311 | /// } |
312 | /// |
313 | /// assert_eq!( |
314 | /// Example { example: " Hello \tworld \t" }.to_string(), |
315 | /// "<div>Hello \tworld</div>" |
316 | /// ); |
317 | /// # } |
318 | /// ``` |
319 | pub fn trim<T: fmt::Display>(s: T) -> Result<String> { |
320 | struct Collector(String); |
321 | |
322 | impl fmt::Write for Collector { |
323 | fn write_str(&mut self, s: &str) -> fmt::Result { |
324 | match self.0.is_empty() { |
325 | true => self.0.write_str(s.trim_start()), |
326 | false => self.0.write_str(s), |
327 | } |
328 | } |
329 | } |
330 | |
331 | let mut collector: Collector = Collector(String::new()); |
332 | write!(collector, " {s}" )?; |
333 | let Collector(mut s: String) = collector; |
334 | s.truncate(new_len:s.trim_end().len()); |
335 | Ok(s) |
336 | } |
337 | |
338 | /// Limit string length, appends '...' if truncated |
339 | /// |
340 | /// ``` |
341 | /// # #[cfg (feature = "code-in-doc" )] { |
342 | /// # use rinja::Template; |
343 | /// /// ```jinja |
344 | /// /// <div>{{ example|truncate(2) }}</div> |
345 | /// /// ``` |
346 | /// #[derive(Template)] |
347 | /// #[template(ext = "html" , in_doc = true)] |
348 | /// struct Example<'a> { |
349 | /// example: &'a str, |
350 | /// } |
351 | /// |
352 | /// assert_eq!( |
353 | /// Example { example: "hello" }.to_string(), |
354 | /// "<div>he...</div>" |
355 | /// ); |
356 | /// # } |
357 | /// ``` |
358 | #[inline ] |
359 | pub fn truncate<S: fmt::Display>( |
360 | source: S, |
361 | remaining: usize, |
362 | ) -> Result<TruncateFilter<S>, Infallible> { |
363 | Ok(TruncateFilter { source, remaining }) |
364 | } |
365 | |
366 | pub struct TruncateFilter<S> { |
367 | source: S, |
368 | remaining: usize, |
369 | } |
370 | |
371 | impl<S: fmt::Display> fmt::Display for TruncateFilter<S> { |
372 | #[inline ] |
373 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
374 | write!(TruncateWriter::new(f, self.remaining), " {}" , self.source) |
375 | } |
376 | } |
377 | |
378 | impl<S: FastWritable> FastWritable for TruncateFilter<S> { |
379 | #[inline ] |
380 | fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> fmt::Result { |
381 | self.source |
382 | .write_into(&mut TruncateWriter::new(dest, self.remaining)) |
383 | } |
384 | } |
385 | |
386 | struct TruncateWriter<W> { |
387 | dest: Option<W>, |
388 | remaining: usize, |
389 | } |
390 | |
391 | impl<W> TruncateWriter<W> { |
392 | fn new(dest: W, remaining: usize) -> Self { |
393 | TruncateWriter { |
394 | dest: Some(dest), |
395 | remaining, |
396 | } |
397 | } |
398 | } |
399 | |
400 | impl<W: fmt::Write> fmt::Write for TruncateWriter<W> { |
401 | fn write_str(&mut self, s: &str) -> fmt::Result { |
402 | let Some(dest) = &mut self.dest else { |
403 | return Ok(()); |
404 | }; |
405 | let mut rem = self.remaining; |
406 | if rem >= s.len() { |
407 | dest.write_str(s)?; |
408 | self.remaining -= s.len(); |
409 | } else { |
410 | if rem > 0 { |
411 | while !s.is_char_boundary(rem) { |
412 | rem += 1; |
413 | } |
414 | if rem == s.len() { |
415 | // Don't write "..." if the char bound extends to the end of string. |
416 | self.remaining = 0; |
417 | return dest.write_str(s); |
418 | } |
419 | dest.write_str(&s[..rem])?; |
420 | } |
421 | dest.write_str("..." )?; |
422 | self.dest = None; |
423 | } |
424 | Ok(()) |
425 | } |
426 | |
427 | #[inline ] |
428 | fn write_char(&mut self, c: char) -> fmt::Result { |
429 | match self.dest.is_some() { |
430 | true => self.write_str(c.encode_utf8(&mut [0; 4])), |
431 | false => Ok(()), |
432 | } |
433 | } |
434 | |
435 | #[inline ] |
436 | fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> fmt::Result { |
437 | match self.dest.is_some() { |
438 | true => fmt::write(self, args), |
439 | false => Ok(()), |
440 | } |
441 | } |
442 | } |
443 | |
444 | /// Indent lines with `width` spaces |
445 | /// |
446 | /// ``` |
447 | /// # #[cfg (feature = "code-in-doc" )] { |
448 | /// # use rinja::Template; |
449 | /// /// ```jinja |
450 | /// /// <div>{{ example|indent(4) }}</div> |
451 | /// /// ``` |
452 | /// #[derive(Template)] |
453 | /// #[template(ext = "html" , in_doc = true)] |
454 | /// struct Example<'a> { |
455 | /// example: &'a str, |
456 | /// } |
457 | /// |
458 | /// assert_eq!( |
459 | /// Example { example: "hello \nfoo \nbar" }.to_string(), |
460 | /// "<div>hello \n foo \n bar</div>" |
461 | /// ); |
462 | /// # } |
463 | /// ``` |
464 | #[inline ] |
465 | pub fn indent(s: impl fmt::Display, width: usize) -> Result<String, fmt::Error> { |
466 | fn indent(s: String, width: usize) -> Result<String, fmt::Error> { |
467 | if width >= MAX_LEN || s.len() >= MAX_LEN { |
468 | return Ok(s); |
469 | } |
470 | let mut indented: String = String::new(); |
471 | for (i: usize, c: char) in s.char_indices() { |
472 | indented.push(ch:c); |
473 | |
474 | if c == ' \n' && i < s.len() - 1 { |
475 | for _ in 0..width { |
476 | indented.push(ch:' ' ); |
477 | } |
478 | } |
479 | } |
480 | Ok(indented) |
481 | } |
482 | indent(s:try_to_string(s)?, width) |
483 | } |
484 | |
485 | /// Joins iterable into a string separated by provided argument |
486 | /// |
487 | /// ``` |
488 | /// # #[cfg (feature = "code-in-doc" )] { |
489 | /// # use rinja::Template; |
490 | /// /// ```jinja |
491 | /// /// <div>{{ example|join(", ") }}</div> |
492 | /// /// ``` |
493 | /// #[derive(Template)] |
494 | /// #[template(ext = "html" , in_doc = true)] |
495 | /// struct Example<'a> { |
496 | /// example: &'a [&'a str], |
497 | /// } |
498 | /// |
499 | /// assert_eq!( |
500 | /// Example { example: &["foo" , "bar" , "bazz" ] }.to_string(), |
501 | /// "<div>foo, bar, bazz</div>" |
502 | /// ); |
503 | /// # } |
504 | /// ``` |
505 | #[inline ] |
506 | pub fn join<I, S>(input: I, separator: S) -> Result<JoinFilter<I, S>, Infallible> |
507 | where |
508 | I: IntoIterator, |
509 | I::Item: fmt::Display, |
510 | S: fmt::Display, |
511 | { |
512 | Ok(JoinFilter(Cell::new(Some((input, separator))))) |
513 | } |
514 | |
515 | /// Result of the filter [`join()`]. |
516 | /// |
517 | /// ## Note |
518 | /// |
519 | /// This struct implements [`fmt::Display`], but only produces a string once. |
520 | /// Any subsequent call to `.to_string()` will result in an empty string, because the iterator is |
521 | /// already consumed. |
522 | // The filter contains a [`Cell`], so we can modify iterator inside a method that takes `self` by |
523 | // reference: [`fmt::Display::fmt()`] normally has the contract that it will produce the same result |
524 | // in multiple invocations for the same object. We break this contract, because have to consume the |
525 | // iterator, unless we want to enforce `I: Clone`, nor do we want to "memorize" the result of the |
526 | // joined data. |
527 | pub struct JoinFilter<I, S>(Cell<Option<(I, S)>>); |
528 | |
529 | impl<I, S> fmt::Display for JoinFilter<I, S> |
530 | where |
531 | I: IntoIterator, |
532 | I::Item: fmt::Display, |
533 | S: fmt::Display, |
534 | { |
535 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
536 | let Some((iter: I, separator: S)) = self.0.take() else { |
537 | return Ok(()); |
538 | }; |
539 | for (idx: usize, token: impl Display) in iter.into_iter().enumerate() { |
540 | match idx { |
541 | 0 => f.write_fmt(format_args!(" {token}" ))?, |
542 | _ => f.write_fmt(format_args!(" {separator}{token}" ))?, |
543 | } |
544 | } |
545 | Ok(()) |
546 | } |
547 | } |
548 | |
549 | /// Capitalize a value. The first character will be uppercase, all others lowercase. |
550 | /// |
551 | /// ``` |
552 | /// # #[cfg (feature = "code-in-doc" )] { |
553 | /// # use rinja::Template; |
554 | /// /// ```jinja |
555 | /// /// <div>{{ example|capitalize }}</div> |
556 | /// /// ``` |
557 | /// #[derive(Template)] |
558 | /// #[template(ext = "html" , in_doc = true)] |
559 | /// struct Example<'a> { |
560 | /// example: &'a str, |
561 | /// } |
562 | /// |
563 | /// assert_eq!( |
564 | /// Example { example: "hello" }.to_string(), |
565 | /// "<div>Hello</div>" |
566 | /// ); |
567 | /// |
568 | /// assert_eq!( |
569 | /// Example { example: "hElLO" }.to_string(), |
570 | /// "<div>Hello</div>" |
571 | /// ); |
572 | /// # } |
573 | /// ``` |
574 | #[inline ] |
575 | pub fn capitalize(s: impl fmt::Display) -> Result<String, fmt::Error> { |
576 | fn capitalize(s: String) -> Result<String, fmt::Error> { |
577 | match s.chars().next() { |
578 | Some(c: char) => { |
579 | let mut replacement: String = c.to_uppercase().collect(); |
580 | replacement.push_str(&s[c.len_utf8()..].to_lowercase()); |
581 | Ok(replacement) |
582 | } |
583 | _ => Ok(s), |
584 | } |
585 | } |
586 | capitalize(try_to_string(s)?) |
587 | } |
588 | |
589 | /// Centers the value in a field of a given width |
590 | /// |
591 | /// ``` |
592 | /// # #[cfg (feature = "code-in-doc" )] { |
593 | /// # use rinja::Template; |
594 | /// /// ```jinja |
595 | /// /// <div>-{{ example|center(5) }}-</div> |
596 | /// /// ``` |
597 | /// #[derive(Template)] |
598 | /// #[template(ext = "html" , in_doc = true)] |
599 | /// struct Example<'a> { |
600 | /// example: &'a str, |
601 | /// } |
602 | /// |
603 | /// assert_eq!( |
604 | /// Example { example: "a" }.to_string(), |
605 | /// "<div>- a -</div>" |
606 | /// ); |
607 | /// # } |
608 | /// ``` |
609 | #[inline ] |
610 | pub fn center<T: fmt::Display>(src: T, width: usize) -> Result<Center<T>, Infallible> { |
611 | Ok(Center { src, width }) |
612 | } |
613 | |
614 | pub struct Center<T> { |
615 | src: T, |
616 | width: usize, |
617 | } |
618 | |
619 | impl<T: fmt::Display> fmt::Display for Center<T> { |
620 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
621 | if self.width < MAX_LEN { |
622 | write!(f, " {: ^1$}" , self.src, self.width) |
623 | } else { |
624 | write!(f, " {}" , self.src) |
625 | } |
626 | } |
627 | } |
628 | |
629 | /// Count the words in that string. |
630 | /// |
631 | /// ``` |
632 | /// # #[cfg (feature = "code-in-doc" )] { |
633 | /// # use rinja::Template; |
634 | /// /// ```jinja |
635 | /// /// <div>{{ example|wordcount }}</div> |
636 | /// /// ``` |
637 | /// #[derive(Template)] |
638 | /// #[template(ext = "html" , in_doc = true)] |
639 | /// struct Example<'a> { |
640 | /// example: &'a str, |
641 | /// } |
642 | /// |
643 | /// assert_eq!( |
644 | /// Example { example: "rinja is sort of cool" }.to_string(), |
645 | /// "<div>5</div>" |
646 | /// ); |
647 | /// # } |
648 | /// ``` |
649 | #[inline ] |
650 | pub fn wordcount(s: impl fmt::Display) -> Result<usize, fmt::Error> { |
651 | fn wordcount(s: String) -> Result<usize, fmt::Error> { |
652 | Ok(s.split_whitespace().count()) |
653 | } |
654 | wordcount(try_to_string(s)?) |
655 | } |
656 | |
657 | /// Return a title cased version of the value. Words will start with uppercase letters, all |
658 | /// remaining characters are lowercase. |
659 | /// |
660 | /// ``` |
661 | /// # #[cfg (feature = "code-in-doc" )] { |
662 | /// # use rinja::Template; |
663 | /// /// ```jinja |
664 | /// /// <div>{{ example|title }}</div> |
665 | /// /// ``` |
666 | /// #[derive(Template)] |
667 | /// #[template(ext = "html" , in_doc = true)] |
668 | /// struct Example<'a> { |
669 | /// example: &'a str, |
670 | /// } |
671 | /// |
672 | /// assert_eq!( |
673 | /// Example { example: "hello WORLD" }.to_string(), |
674 | /// "<div>Hello World</div>" |
675 | /// ); |
676 | /// # } |
677 | /// ``` |
678 | pub fn title(s: impl fmt::Display) -> Result<String, fmt::Error> { |
679 | let s = try_to_string(s)?; |
680 | let mut need_capitalization = true; |
681 | |
682 | // Sadly enough, we can't mutate a string when iterating over its chars, likely because it could |
683 | // change the size of a char, "breaking" the char indices. |
684 | let mut output = String::with_capacity(s.len()); |
685 | for c in s.chars() { |
686 | if c.is_whitespace() { |
687 | output.push(c); |
688 | need_capitalization = true; |
689 | } else if need_capitalization { |
690 | match c.is_uppercase() { |
691 | true => output.push(c), |
692 | false => output.extend(c.to_uppercase()), |
693 | } |
694 | need_capitalization = false; |
695 | } else { |
696 | match c.is_lowercase() { |
697 | true => output.push(c), |
698 | false => output.extend(c.to_lowercase()), |
699 | } |
700 | } |
701 | } |
702 | Ok(output) |
703 | } |
704 | |
705 | /// For a value of `±1` by default an empty string `""` is returned, otherwise `"s"`. |
706 | /// |
707 | /// # Examples |
708 | /// |
709 | /// ## With default arguments |
710 | /// |
711 | /// ``` |
712 | /// # #[cfg (feature = "code-in-doc" )] { |
713 | /// # use rinja::Template; |
714 | /// /// ```jinja |
715 | /// /// I have {{dogs}} dog{{dogs|pluralize}} and {{cats}} cat{{cats|pluralize}}. |
716 | /// /// ``` |
717 | /// #[derive(Template)] |
718 | /// #[template(ext = "html" , in_doc = true)] |
719 | /// struct Pets { |
720 | /// dogs: i8, |
721 | /// cats: i8, |
722 | /// } |
723 | /// |
724 | /// assert_eq!( |
725 | /// Pets { dogs: 0, cats: 0 }.to_string(), |
726 | /// "I have 0 dogs and 0 cats." |
727 | /// ); |
728 | /// assert_eq!( |
729 | /// Pets { dogs: 1, cats: 1 }.to_string(), |
730 | /// "I have 1 dog and 1 cat." |
731 | /// ); |
732 | /// assert_eq!( |
733 | /// Pets { dogs: -1, cats: 99 }.to_string(), |
734 | /// "I have -1 dog and 99 cats." |
735 | /// ); |
736 | /// # } |
737 | /// ``` |
738 | /// |
739 | /// ## Overriding the singular case |
740 | /// |
741 | /// ``` |
742 | /// # #[cfg (feature = "code-in-doc" )] { |
743 | /// # use rinja::Template; |
744 | /// /// ```jinja |
745 | /// /// I have {{dogs}} dog{{ dogs|pluralize("go") }}. |
746 | /// /// ``` |
747 | /// #[derive(Template)] |
748 | /// #[template(ext = "html" , in_doc = true)] |
749 | /// struct Dog { |
750 | /// dogs: i8, |
751 | /// } |
752 | /// |
753 | /// assert_eq!( |
754 | /// Dog { dogs: 0 }.to_string(), |
755 | /// "I have 0 dogs." |
756 | /// ); |
757 | /// assert_eq!( |
758 | /// Dog { dogs: 1 }.to_string(), |
759 | /// "I have 1 doggo." |
760 | /// ); |
761 | /// # } |
762 | /// ``` |
763 | /// |
764 | /// ## Overriding singular and plural cases |
765 | /// |
766 | /// ``` |
767 | /// # #[cfg (feature = "code-in-doc" )] { |
768 | /// # use rinja::Template; |
769 | /// /// ```jinja |
770 | /// /// I have {{mice}} {{ mice|pluralize("mouse", "mice") }}. |
771 | /// /// ``` |
772 | /// #[derive(Template)] |
773 | /// #[template(ext = "html" , in_doc = true)] |
774 | /// struct Mice { |
775 | /// mice: i8, |
776 | /// } |
777 | /// |
778 | /// assert_eq!( |
779 | /// Mice { mice: 42 }.to_string(), |
780 | /// "I have 42 mice." |
781 | /// ); |
782 | /// assert_eq!( |
783 | /// Mice { mice: 1 }.to_string(), |
784 | /// "I have 1 mouse." |
785 | /// ); |
786 | /// # } |
787 | /// ``` |
788 | /// |
789 | /// ## Arguments get escaped |
790 | /// |
791 | /// ``` |
792 | /// # #[cfg (feature = "code-in-doc" )] { |
793 | /// # use rinja::Template; |
794 | /// /// ```jinja |
795 | /// /// You are number {{ number|pluralize("<b>ONE</b>", number) }}! |
796 | /// /// ``` |
797 | /// #[derive(Template)] |
798 | /// #[template(ext = "html" , in_doc = true)] |
799 | /// struct Number { |
800 | /// number: usize |
801 | /// } |
802 | /// |
803 | /// assert_eq!( |
804 | /// Number { number: 1 }.to_string(), |
805 | /// "You are number <b>ONE</b>!" , |
806 | /// ); |
807 | /// assert_eq!( |
808 | /// Number { number: 9000 }.to_string(), |
809 | /// "You are number 9000!" , |
810 | /// ); |
811 | /// # } |
812 | /// ``` |
813 | #[inline ] |
814 | pub fn pluralize<C, S, P>(count: C, singular: S, plural: P) -> Result<Pluralize<S, P>, C::Error> |
815 | where |
816 | C: PluralizeCount, |
817 | { |
818 | match count.is_singular()? { |
819 | true => Ok(Pluralize::Singular(singular)), |
820 | false => Ok(Pluralize::Plural(plural)), |
821 | } |
822 | } |
823 | |
824 | /// An integer that can have the value `+1` and maybe `-1`. |
825 | pub trait PluralizeCount { |
826 | /// A possible error that can occur while checking the value. |
827 | type Error: Into<Error>; |
828 | |
829 | /// Returns `true` if and only if the value is `±1`. |
830 | fn is_singular(&self) -> Result<bool, Self::Error>; |
831 | } |
832 | |
833 | const _: () = { |
834 | crate::impl_for_ref! { |
835 | impl PluralizeCount for T { |
836 | type Error = T::Error; |
837 | |
838 | #[inline ] |
839 | fn is_singular(&self) -> Result<bool, Self::Error> { |
840 | <T>::is_singular(self) |
841 | } |
842 | } |
843 | } |
844 | |
845 | impl<T> PluralizeCount for Pin<T> |
846 | where |
847 | T: Deref, |
848 | <T as Deref>::Target: PluralizeCount, |
849 | { |
850 | type Error = <<T as Deref>::Target as PluralizeCount>::Error; |
851 | |
852 | #[inline ] |
853 | fn is_singular(&self) -> Result<bool, Self::Error> { |
854 | self.as_ref().get_ref().is_singular() |
855 | } |
856 | } |
857 | |
858 | /// implement `PluralizeCount` for unsigned integer types |
859 | macro_rules! impl_pluralize_for_unsigned_int { |
860 | ($($ty:ty)*) => { $( |
861 | impl PluralizeCount for $ty { |
862 | type Error = Infallible; |
863 | |
864 | #[inline] |
865 | fn is_singular(&self) -> Result<bool, Self::Error> { |
866 | Ok(*self == 1) |
867 | } |
868 | } |
869 | )* }; |
870 | } |
871 | |
872 | impl_pluralize_for_unsigned_int!(u8 u16 u32 u64 u128 usize); |
873 | |
874 | /// implement `PluralizeCount` for signed integer types |
875 | macro_rules! impl_pluralize_for_signed_int { |
876 | ($($ty:ty)*) => { $( |
877 | impl PluralizeCount for $ty { |
878 | type Error = Infallible; |
879 | |
880 | #[inline] |
881 | fn is_singular(&self) -> Result<bool, Self::Error> { |
882 | Ok(*self == 1 || *self == -1) |
883 | } |
884 | } |
885 | )* }; |
886 | } |
887 | |
888 | impl_pluralize_for_signed_int!(i8 i16 i32 i64 i128 isize); |
889 | |
890 | /// implement `PluralizeCount` for non-zero integer types |
891 | macro_rules! impl_pluralize_for_non_zero { |
892 | ($($ty:ident)*) => { $( |
893 | impl PluralizeCount for std::num::$ty { |
894 | type Error = Infallible; |
895 | |
896 | #[inline] |
897 | fn is_singular(&self) -> Result<bool, Self::Error> { |
898 | self.get().is_singular() |
899 | } |
900 | } |
901 | )* }; |
902 | } |
903 | |
904 | impl_pluralize_for_non_zero! { |
905 | NonZeroI8 NonZeroI16 NonZeroI32 NonZeroI64 NonZeroI128 NonZeroIsize |
906 | NonZeroU8 NonZeroU16 NonZeroU32 NonZeroU64 NonZeroU128 NonZeroUsize |
907 | } |
908 | }; |
909 | |
910 | pub enum Pluralize<S, P> { |
911 | Singular(S), |
912 | Plural(P), |
913 | } |
914 | |
915 | impl<S: fmt::Display, P: fmt::Display> fmt::Display for Pluralize<S, P> { |
916 | #[inline ] |
917 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
918 | match self { |
919 | Pluralize::Singular(value: &S) => write!(f, " {value}" ), |
920 | Pluralize::Plural(value: &P) => write!(f, " {value}" ), |
921 | } |
922 | } |
923 | } |
924 | |
925 | impl<S: FastWritable, P: FastWritable> FastWritable for Pluralize<S, P> { |
926 | #[inline ] |
927 | fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> fmt::Result { |
928 | match self { |
929 | Pluralize::Singular(value: &S) => value.write_into(dest), |
930 | Pluralize::Plural(value: &P) => value.write_into(dest), |
931 | } |
932 | } |
933 | } |
934 | |
935 | fn try_to_string(s: impl fmt::Display) -> Result<String, fmt::Error> { |
936 | let mut result: String = String::new(); |
937 | write!(result, " {s}" )?; |
938 | Ok(result) |
939 | } |
940 | |
941 | #[cfg (test)] |
942 | mod tests { |
943 | use super::*; |
944 | |
945 | #[test ] |
946 | fn test_linebreaks() { |
947 | assert_eq!( |
948 | linebreaks("Foo \nBar Baz" ).unwrap().to_string(), |
949 | "<p>Foo<br/>Bar Baz</p>" |
950 | ); |
951 | assert_eq!( |
952 | linebreaks("Foo \nBar \n\nBaz" ).unwrap().to_string(), |
953 | "<p>Foo<br/>Bar</p><p>Baz</p>" |
954 | ); |
955 | } |
956 | |
957 | #[test ] |
958 | fn test_linebreaksbr() { |
959 | assert_eq!(linebreaksbr("Foo \nBar" ).unwrap().to_string(), "Foo<br/>Bar" ); |
960 | assert_eq!( |
961 | linebreaksbr("Foo \nBar \n\nBaz" ).unwrap().to_string(), |
962 | "Foo<br/>Bar<br/><br/>Baz" |
963 | ); |
964 | } |
965 | |
966 | #[test ] |
967 | fn test_paragraphbreaks() { |
968 | assert_eq!( |
969 | paragraphbreaks("Foo \nBar Baz" ).unwrap().to_string(), |
970 | "<p>Foo \nBar Baz</p>" |
971 | ); |
972 | assert_eq!( |
973 | paragraphbreaks("Foo \nBar \n\nBaz" ).unwrap().to_string(), |
974 | "<p>Foo \nBar</p><p>Baz</p>" |
975 | ); |
976 | assert_eq!( |
977 | paragraphbreaks("Foo \n\n\n\n\nBar \n\nBaz" ) |
978 | .unwrap() |
979 | .to_string(), |
980 | "<p>Foo</p><p> \nBar</p><p>Baz</p>" |
981 | ); |
982 | } |
983 | |
984 | #[test ] |
985 | fn test_lower() { |
986 | assert_eq!(lower("Foo" ).unwrap().to_string(), "foo" ); |
987 | assert_eq!(lower("FOO" ).unwrap().to_string(), "foo" ); |
988 | assert_eq!(lower("FooBar" ).unwrap().to_string(), "foobar" ); |
989 | assert_eq!(lower("foo" ).unwrap().to_string(), "foo" ); |
990 | } |
991 | |
992 | #[test ] |
993 | fn test_upper() { |
994 | assert_eq!(upper("Foo" ).unwrap().to_string(), "FOO" ); |
995 | assert_eq!(upper("FOO" ).unwrap().to_string(), "FOO" ); |
996 | assert_eq!(upper("FooBar" ).unwrap().to_string(), "FOOBAR" ); |
997 | assert_eq!(upper("foo" ).unwrap().to_string(), "FOO" ); |
998 | } |
999 | |
1000 | #[test ] |
1001 | fn test_trim() { |
1002 | assert_eq!(trim(" Hello \tworld \t" ).unwrap().to_string(), "Hello \tworld" ); |
1003 | } |
1004 | |
1005 | #[test ] |
1006 | fn test_truncate() { |
1007 | assert_eq!(truncate("hello" , 2).unwrap().to_string(), "he..." ); |
1008 | let a = String::from("您好" ); |
1009 | assert_eq!(a.len(), 6); |
1010 | assert_eq!(String::from("您" ).len(), 3); |
1011 | assert_eq!(truncate("您好" , 1).unwrap().to_string(), "您..." ); |
1012 | assert_eq!(truncate("您好" , 2).unwrap().to_string(), "您..." ); |
1013 | assert_eq!(truncate("您好" , 3).unwrap().to_string(), "您..." ); |
1014 | assert_eq!(truncate("您好" , 4).unwrap().to_string(), "您好" ); |
1015 | assert_eq!(truncate("您好" , 5).unwrap().to_string(), "您好" ); |
1016 | assert_eq!(truncate("您好" , 6).unwrap().to_string(), "您好" ); |
1017 | assert_eq!(truncate("您好" , 7).unwrap().to_string(), "您好" ); |
1018 | let s = String::from("🤚a🤚" ); |
1019 | assert_eq!(s.len(), 9); |
1020 | assert_eq!(String::from("🤚" ).len(), 4); |
1021 | assert_eq!(truncate("🤚a🤚" , 1).unwrap().to_string(), "🤚..." ); |
1022 | assert_eq!(truncate("🤚a🤚" , 2).unwrap().to_string(), "🤚..." ); |
1023 | assert_eq!(truncate("🤚a🤚" , 3).unwrap().to_string(), "🤚..." ); |
1024 | assert_eq!(truncate("🤚a🤚" , 4).unwrap().to_string(), "🤚..." ); |
1025 | assert_eq!(truncate("🤚a🤚" , 5).unwrap().to_string(), "🤚a..." ); |
1026 | assert_eq!(truncate("🤚a🤚" , 6).unwrap().to_string(), "🤚a🤚" ); |
1027 | assert_eq!(truncate("🤚a🤚" , 6).unwrap().to_string(), "🤚a🤚" ); |
1028 | assert_eq!(truncate("🤚a🤚" , 7).unwrap().to_string(), "🤚a🤚" ); |
1029 | assert_eq!(truncate("🤚a🤚" , 8).unwrap().to_string(), "🤚a🤚" ); |
1030 | assert_eq!(truncate("🤚a🤚" , 9).unwrap().to_string(), "🤚a🤚" ); |
1031 | assert_eq!(truncate("🤚a🤚" , 10).unwrap().to_string(), "🤚a🤚" ); |
1032 | } |
1033 | |
1034 | #[test ] |
1035 | fn test_indent() { |
1036 | assert_eq!(indent("hello" , 2).unwrap().to_string(), "hello" ); |
1037 | assert_eq!(indent("hello \n" , 2).unwrap().to_string(), "hello \n" ); |
1038 | assert_eq!(indent("hello \nfoo" , 2).unwrap().to_string(), "hello \n foo" ); |
1039 | assert_eq!( |
1040 | indent("hello \nfoo \n bar" , 4).unwrap().to_string(), |
1041 | "hello \n foo \n bar" |
1042 | ); |
1043 | assert_eq!( |
1044 | indent("hello" , 267_332_238_858).unwrap().to_string(), |
1045 | "hello" |
1046 | ); |
1047 | } |
1048 | |
1049 | #[allow (clippy::needless_borrow)] |
1050 | #[test ] |
1051 | fn test_join() { |
1052 | assert_eq!( |
1053 | join((&["hello" , "world" ]).iter(), ", " ) |
1054 | .unwrap() |
1055 | .to_string(), |
1056 | "hello, world" |
1057 | ); |
1058 | assert_eq!( |
1059 | join((&["hello" ]).iter(), ", " ).unwrap().to_string(), |
1060 | "hello" |
1061 | ); |
1062 | |
1063 | let empty: &[&str] = &[]; |
1064 | assert_eq!(join(empty.iter(), ", " ).unwrap().to_string(), "" ); |
1065 | |
1066 | let input: Vec<String> = vec!["foo" .into(), "bar" .into(), "bazz" .into()]; |
1067 | assert_eq!(join(input.iter(), ":" ).unwrap().to_string(), "foo:bar:bazz" ); |
1068 | |
1069 | let input: &[String] = &["foo" .into(), "bar" .into()]; |
1070 | assert_eq!(join(input.iter(), ":" ).unwrap().to_string(), "foo:bar" ); |
1071 | |
1072 | let real: String = "blah" .into(); |
1073 | let input: Vec<&str> = vec![&real]; |
1074 | assert_eq!(join(input.iter(), ";" ).unwrap().to_string(), "blah" ); |
1075 | |
1076 | assert_eq!( |
1077 | join((&&&&&["foo" , "bar" ]).iter(), ", " ) |
1078 | .unwrap() |
1079 | .to_string(), |
1080 | "foo, bar" |
1081 | ); |
1082 | } |
1083 | |
1084 | #[test ] |
1085 | fn test_capitalize() { |
1086 | assert_eq!(capitalize("foo" ).unwrap().to_string(), "Foo" .to_string()); |
1087 | assert_eq!(capitalize("f" ).unwrap().to_string(), "F" .to_string()); |
1088 | assert_eq!(capitalize("fO" ).unwrap().to_string(), "Fo" .to_string()); |
1089 | assert_eq!(capitalize("" ).unwrap().to_string(), String::new()); |
1090 | assert_eq!(capitalize("FoO" ).unwrap().to_string(), "Foo" .to_string()); |
1091 | assert_eq!( |
1092 | capitalize("foO BAR" ).unwrap().to_string(), |
1093 | "Foo bar" .to_string() |
1094 | ); |
1095 | assert_eq!( |
1096 | capitalize("äØÄÅÖ" ).unwrap().to_string(), |
1097 | "Äøäåö" .to_string() |
1098 | ); |
1099 | assert_eq!(capitalize("ß" ).unwrap().to_string(), "SS" .to_string()); |
1100 | assert_eq!(capitalize("ßß" ).unwrap().to_string(), "SSß" .to_string()); |
1101 | } |
1102 | |
1103 | #[test ] |
1104 | fn test_center() { |
1105 | assert_eq!(center("f" , 3).unwrap().to_string(), " f " .to_string()); |
1106 | assert_eq!(center("f" , 4).unwrap().to_string(), " f " .to_string()); |
1107 | assert_eq!(center("foo" , 1).unwrap().to_string(), "foo" .to_string()); |
1108 | assert_eq!( |
1109 | center("foo bar" , 8).unwrap().to_string(), |
1110 | "foo bar " .to_string() |
1111 | ); |
1112 | assert_eq!( |
1113 | center("foo" , 111_669_149_696).unwrap().to_string(), |
1114 | "foo" .to_string() |
1115 | ); |
1116 | } |
1117 | |
1118 | #[test ] |
1119 | fn test_wordcount() { |
1120 | assert_eq!(wordcount("" ).unwrap(), 0); |
1121 | assert_eq!(wordcount(" \n\t" ).unwrap(), 0); |
1122 | assert_eq!(wordcount("foo" ).unwrap(), 1); |
1123 | assert_eq!(wordcount("foo bar" ).unwrap(), 2); |
1124 | assert_eq!(wordcount("foo bar" ).unwrap(), 2); |
1125 | } |
1126 | |
1127 | #[test ] |
1128 | fn test_title() { |
1129 | assert_eq!(&title("" ).unwrap(), "" ); |
1130 | assert_eq!(&title(" \n\t" ).unwrap(), " \n\t" ); |
1131 | assert_eq!(&title("foo" ).unwrap(), "Foo" ); |
1132 | assert_eq!(&title(" foo" ).unwrap(), " Foo" ); |
1133 | assert_eq!(&title("foo bar" ).unwrap(), "Foo Bar" ); |
1134 | assert_eq!(&title("foo bar " ).unwrap(), "Foo Bar " ); |
1135 | assert_eq!(&title("fOO" ).unwrap(), "Foo" ); |
1136 | assert_eq!(&title("fOo BaR" ).unwrap(), "Foo Bar" ); |
1137 | } |
1138 | |
1139 | #[test ] |
1140 | fn fuzzed_indent_filter() { |
1141 | let s = "hello \nfoo \nbar" .to_string().repeat(1024); |
1142 | assert_eq!(indent(s.clone(), 4).unwrap().to_string(), s); |
1143 | } |
1144 | } |
1145 | |