1use std::cell::Cell;
2use std::convert::Infallible;
3use std::fmt::{self, Write};
4use std::ops::Deref;
5use std::pin::Pin;
6
7use super::escape::{FastWritable, HtmlSafeOutput};
8use crate::{Error, Result};
9
10// MAX_LEN is maximum allowed length for filters.
11const 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).
45pub 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).
78pub 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]
104pub 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]
133pub 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]
165pub 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]
199pub 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]
232pub 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]
262pub 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]
295pub 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/// ```
319pub 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]
359pub fn truncate<S: fmt::Display>(
360 source: S,
361 remaining: usize,
362) -> Result<TruncateFilter<S>, Infallible> {
363 Ok(TruncateFilter { source, remaining })
364}
365
366pub struct TruncateFilter<S> {
367 source: S,
368 remaining: usize,
369}
370
371impl<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
378impl<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
386struct TruncateWriter<W> {
387 dest: Option<W>,
388 remaining: usize,
389}
390
391impl<W> TruncateWriter<W> {
392 fn new(dest: W, remaining: usize) -> Self {
393 TruncateWriter {
394 dest: Some(dest),
395 remaining,
396 }
397 }
398}
399
400impl<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]
465pub 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]
506pub fn join<I, S>(input: I, separator: S) -> Result<JoinFilter<I, S>, Infallible>
507where
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.
527pub struct JoinFilter<I, S>(Cell<Option<(I, S)>>);
528
529impl<I, S> fmt::Display for JoinFilter<I, S>
530where
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]
575pub 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]
610pub fn center<T: fmt::Display>(src: T, width: usize) -> Result<Center<T>, Infallible> {
611 Ok(Center { src, width })
612}
613
614pub struct Center<T> {
615 src: T,
616 width: usize,
617}
618
619impl<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]
650pub 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/// ```
678pub 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 &#60;b&#62;ONE&#60;/b&#62;!",
806/// );
807/// assert_eq!(
808/// Number { number: 9000 }.to_string(),
809/// "You are number 9000!",
810/// );
811/// # }
812/// ```
813#[inline]
814pub fn pluralize<C, S, P>(count: C, singular: S, plural: P) -> Result<Pluralize<S, P>, C::Error>
815where
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`.
825pub 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
833const _: () = {
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
910pub enum Pluralize<S, P> {
911 Singular(S),
912 Plural(P),
913}
914
915impl<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
925impl<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
935fn 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)]
942mod 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