diff --git a/rinja/src/filters/alloc.rs b/rinja/src/filters/alloc.rs new file mode 100644 index 00000000..18488531 --- /dev/null +++ b/rinja/src/filters/alloc.rs @@ -0,0 +1,1178 @@ +use std::cell::Cell; +use std::convert::Infallible; +use std::fmt::{self, Write}; +use std::ops::Deref; +use std::pin::Pin; + +use super::escape::{FastWritable, HtmlSafeOutput}; +use crate::{Error, Result}; + +// MAX_LEN is maximum allowed length for filters. +const MAX_LEN: usize = 10_000; + +/// Return an ephemeral `&str` for `$src: impl fmt::Display` +/// +/// If `$str` is `&str` or `String`, this macro simply passes on its content. +/// If it is neither, then the formatted data is collection into `&buffer`. +/// +/// `return`s with an error if the formatting failed. +macro_rules! try_to_str { + ($src:expr => $buffer:ident) => { + match format_args!("{}", $src) { + args => { + if let Some(s) = args.as_str() { + s + } else { + $buffer = String::new(); + $buffer.write_fmt(args)?; + &$buffer + } + } + } + }; +} + +/// Formats arguments according to the specified format +/// +/// The *second* argument to this filter must be a string literal (as in normal +/// Rust). The two arguments are passed through to the `format!()` +/// [macro](https://doc.rust-lang.org/stable/std/macro.format.html) by +/// the Rinja code generator, but the order is swapped to support filter +/// composition. +/// +/// ```ignore +/// {{ value|fmt("{:?}") }} +/// ``` +/// +/// ``` +/// # #[cfg(feature = "code-in-doc")] { +/// # use rinja::Template; +/// /// ```jinja +/// ///
{{ value|fmt("{:?}") }}
+/// /// ``` +/// #[derive(Template)] +/// #[template(ext = "html", in_doc = true)] +/// struct Example { +/// value: (usize, usize), +/// } +/// +/// assert_eq!( +/// Example { value: (3, 4) }.to_string(), +/// "
(3, 4)
" +/// ); +/// # } +/// ``` +/// +/// Compare with [format](./fn.format.html). +pub fn fmt() {} + +/// Formats arguments according to the specified format +/// +/// The first argument to this filter must be a string literal (as in normal +/// Rust). All arguments are passed through to the `format!()` +/// [macro](https://doc.rust-lang.org/stable/std/macro.format.html) by +/// the Rinja code generator. +/// +/// ```ignore +/// {{ "{:?}{:?}"|format(value, other_value) }} +/// ``` +/// +/// ``` +/// # #[cfg(feature = "code-in-doc")] { +/// # use rinja::Template; +/// /// ```jinja +/// ///
{{ "{:?}"|format(value) }}
+/// /// ``` +/// #[derive(Template)] +/// #[template(ext = "html", in_doc = true)] +/// struct Example { +/// value: (usize, usize), +/// } +/// +/// assert_eq!( +/// Example { value: (3, 4) }.to_string(), +/// "
(3, 4)
" +/// ); +/// # } +/// ``` +/// +/// Compare with [fmt](./fn.fmt.html). +pub fn format() {} + +/// Replaces line breaks in plain text with appropriate HTML +/// +/// A single newline becomes an HTML line break `
` and a new line +/// followed by a blank line becomes a paragraph break `

`. +/// +/// ``` +/// # #[cfg(feature = "code-in-doc")] { +/// # use rinja::Template; +/// /// ```jinja +/// ///

{{ example|linebreaks }}
+/// /// ``` +/// #[derive(Template)] +/// #[template(ext = "html", in_doc = true)] +/// struct Example<'a> { +/// example: &'a str, +/// } +/// +/// assert_eq!( +/// Example { example: "Foo\nBar\n\nBaz" }.to_string(), +/// "

Foo
Bar

Baz

" +/// ); +/// # } +/// ``` +#[inline] +pub fn linebreaks(s: impl fmt::Display) -> Result, fmt::Error> { + fn linebreaks(s: &str) -> String { + let linebroken = s.replace("\n\n", "

").replace('\n', "
"); + format!("

{linebroken}

") + } + + let mut buffer; + Ok(HtmlSafeOutput(linebreaks(try_to_str!(s => buffer)))) +} + +/// Converts all newlines in a piece of plain text to HTML line breaks +/// +/// ``` +/// # #[cfg(feature = "code-in-doc")] { +/// # use rinja::Template; +/// /// ```jinja +/// ///
{{ lines|linebreaksbr }}
+/// /// ``` +/// #[derive(Template)] +/// #[template(ext = "html", in_doc = true)] +/// struct Example<'a> { +/// lines: &'a str, +/// } +/// +/// assert_eq!( +/// Example { lines: "a\nb\nc" }.to_string(), +/// "
a
b
c
" +/// ); +/// # } +/// ``` +#[inline] +pub fn linebreaksbr(s: impl fmt::Display) -> Result, fmt::Error> { + fn linebreaksbr(s: &str) -> String { + s.replace('\n', "
") + } + + let mut buffer; + Ok(HtmlSafeOutput(linebreaksbr(try_to_str!(s => buffer)))) +} + +/// Replaces only paragraph breaks in plain text with appropriate HTML +/// +/// A new line followed by a blank line becomes a paragraph break `

`. +/// Paragraph tags only wrap content; empty paragraphs are removed. +/// No `
` tags are added. +/// +/// ``` +/// # #[cfg(feature = "code-in-doc")] { +/// # use rinja::Template; +/// /// ```jinja +/// /// {{ lines|paragraphbreaks }} +/// /// ``` +/// #[derive(Template)] +/// #[template(ext = "html", in_doc = true)] +/// struct Example<'a> { +/// lines: &'a str, +/// } +/// +/// assert_eq!( +/// Example { lines: "Foo\nBar\n\nBaz" }.to_string(), +/// "

Foo\nBar

Baz

" +/// ); +/// # } +/// ``` +#[inline] +pub fn paragraphbreaks(s: impl fmt::Display) -> Result, fmt::Error> { + fn paragraphbreaks(s: &str) -> String { + let linebroken = s.replace("\n\n", "

").replace("

", ""); + format!("

{linebroken}

") + } + + let mut buffer; + Ok(HtmlSafeOutput(paragraphbreaks(try_to_str!(s => buffer)))) +} + +/// Converts to lowercase +/// +/// ``` +/// # #[cfg(feature = "code-in-doc")] { +/// # use rinja::Template; +/// /// ```jinja +/// ///
{{ word|lower }}
+/// /// ``` +/// #[derive(Template)] +/// #[template(ext = "html", in_doc = true)] +/// struct Example<'a> { +/// word: &'a str, +/// } +/// +/// assert_eq!( +/// Example { word: "FOO" }.to_string(), +/// "
foo
" +/// ); +/// +/// assert_eq!( +/// Example { word: "FooBar" }.to_string(), +/// "
foobar
" +/// ); +/// # } +/// ``` +#[inline] +pub fn lower(s: impl fmt::Display) -> Result { + let mut buffer; + Ok(try_to_str!(s => buffer).to_lowercase()) +} + +/// Converts to lowercase, alias for the `|lower` filter +/// +/// ``` +/// # #[cfg(feature = "code-in-doc")] { +/// # use rinja::Template; +/// /// ```jinja +/// ///
{{ word|lowercase }}
+/// /// ``` +/// #[derive(Template)] +/// #[template(ext = "html", in_doc = true)] +/// struct Example<'a> { +/// word: &'a str, +/// } +/// +/// assert_eq!( +/// Example { word: "FOO" }.to_string(), +/// "
foo
" +/// ); +/// +/// assert_eq!( +/// Example { word: "FooBar" }.to_string(), +/// "
foobar
" +/// ); +/// # } +/// ``` +#[inline] +pub fn lowercase(s: impl fmt::Display) -> Result { + lower(s) +} + +/// Converts to uppercase +/// +/// ``` +/// # #[cfg(feature = "code-in-doc")] { +/// # use rinja::Template; +/// /// ```jinja +/// ///
{{ word|upper }}
+/// /// ``` +/// #[derive(Template)] +/// #[template(ext = "html", in_doc = true)] +/// struct Example<'a> { +/// word: &'a str, +/// } +/// +/// assert_eq!( +/// Example { word: "foo" }.to_string(), +/// "
FOO
" +/// ); +/// +/// assert_eq!( +/// Example { word: "FooBar" }.to_string(), +/// "
FOOBAR
" +/// ); +/// # } +/// ``` +#[inline] +pub fn upper(s: impl fmt::Display) -> Result { + let mut buffer; + Ok(try_to_str!(s => buffer).to_uppercase()) +} + +/// Converts to uppercase, alias for the `|upper` filter +/// +/// ``` +/// # #[cfg(feature = "code-in-doc")] { +/// # use rinja::Template; +/// /// ```jinja +/// ///
{{ word|uppercase }}
+/// /// ``` +/// #[derive(Template)] +/// #[template(ext = "html", in_doc = true)] +/// struct Example<'a> { +/// word: &'a str, +/// } +/// +/// assert_eq!( +/// Example { word: "foo" }.to_string(), +/// "
FOO
" +/// ); +/// +/// assert_eq!( +/// Example { word: "FooBar" }.to_string(), +/// "
FOOBAR
" +/// ); +/// # } +/// ``` +#[inline] +pub fn uppercase(s: impl fmt::Display) -> Result { + upper(s) +} + +/// Strip leading and trailing whitespace +/// +/// ``` +/// # #[cfg(feature = "code-in-doc")] { +/// # use rinja::Template; +/// /// ```jinja +/// ///
{{ example|trim }}
+/// /// ``` +/// #[derive(Template)] +/// #[template(ext = "html", in_doc = true)] +/// struct Example<'a> { +/// example: &'a str, +/// } +/// +/// assert_eq!( +/// Example { example: " Hello\tworld\t" }.to_string(), +/// "
Hello\tworld
" +/// ); +/// # } +/// ``` +pub fn trim(s: T) -> Result { + struct Collector(String); + + impl fmt::Write for Collector { + fn write_str(&mut self, s: &str) -> fmt::Result { + match self.0.is_empty() { + true => self.0.write_str(s.trim_start()), + false => self.0.write_str(s), + } + } + } + + let mut collector = Collector(String::new()); + write!(collector, "{s}")?; + let Collector(mut s) = collector; + s.truncate(s.trim_end().len()); + Ok(s) +} + +/// Limit string length, appends '...' if truncated +/// +/// ``` +/// # #[cfg(feature = "code-in-doc")] { +/// # use rinja::Template; +/// /// ```jinja +/// ///
{{ example|truncate(2) }}
+/// /// ``` +/// #[derive(Template)] +/// #[template(ext = "html", in_doc = true)] +/// struct Example<'a> { +/// example: &'a str, +/// } +/// +/// assert_eq!( +/// Example { example: "hello" }.to_string(), +/// "
he...
" +/// ); +/// # } +/// ``` +#[inline] +pub fn truncate( + source: S, + remaining: usize, +) -> Result, Infallible> { + Ok(TruncateFilter { source, remaining }) +} + +pub struct TruncateFilter { + source: S, + remaining: usize, +} + +impl fmt::Display for TruncateFilter { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(TruncateWriter::new(f, self.remaining), "{}", self.source) + } +} + +impl FastWritable for TruncateFilter { + #[inline] + fn write_into(&self, dest: &mut W) -> crate::Result<()> { + self.source + .write_into(&mut TruncateWriter::new(dest, self.remaining)) + } +} + +struct TruncateWriter { + dest: Option, + remaining: usize, +} + +impl TruncateWriter { + fn new(dest: W, remaining: usize) -> Self { + TruncateWriter { + dest: Some(dest), + remaining, + } + } +} + +impl fmt::Write for TruncateWriter { + fn write_str(&mut self, s: &str) -> fmt::Result { + let Some(dest) = &mut self.dest else { + return Ok(()); + }; + let mut rem = self.remaining; + if rem >= s.len() { + dest.write_str(s)?; + self.remaining -= s.len(); + } else { + if rem > 0 { + while !s.is_char_boundary(rem) { + rem += 1; + } + if rem == s.len() { + // Don't write "..." if the char bound extends to the end of string. + self.remaining = 0; + return dest.write_str(s); + } + dest.write_str(&s[..rem])?; + } + dest.write_str("...")?; + self.dest = None; + } + Ok(()) + } + + #[inline] + fn write_char(&mut self, c: char) -> fmt::Result { + match self.dest.is_some() { + true => self.write_str(c.encode_utf8(&mut [0; 4])), + false => Ok(()), + } + } + + #[inline] + fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> fmt::Result { + match self.dest.is_some() { + true => fmt::write(self, args), + false => Ok(()), + } + } +} + +/// Indent lines with `width` spaces +/// +/// ``` +/// # #[cfg(feature = "code-in-doc")] { +/// # use rinja::Template; +/// /// ```jinja +/// ///
{{ example|indent(4) }}
+/// /// ``` +/// #[derive(Template)] +/// #[template(ext = "html", in_doc = true)] +/// struct Example<'a> { +/// example: &'a str, +/// } +/// +/// assert_eq!( +/// Example { example: "hello\nfoo\nbar" }.to_string(), +/// "
hello\n foo\n bar
" +/// ); +/// # } +/// ``` +#[inline] +pub fn indent(s: impl fmt::Display, width: usize) -> Result { + fn indent(args: fmt::Arguments<'_>, width: usize) -> Result { + let mut buffer = String::new(); + let s = if width >= MAX_LEN { + buffer.write_fmt(args)?; + return Ok(buffer); + } else if let Some(s) = args.as_str() { + if s.len() >= MAX_LEN { + return Ok(s.to_owned()); + } else { + s + } + } else { + buffer.write_fmt(args)?; + if buffer.len() >= MAX_LEN { + return Ok(buffer); + } + buffer.as_str() + }; + + let mut indented = String::new(); + for (i, c) in s.char_indices() { + indented.push(c); + + if c == '\n' && i < s.len() - 1 { + for _ in 0..width { + indented.push(' '); + } + } + } + Ok(indented) + } + indent(format_args!("{s}"), width) +} + +/// Joins iterable into a string separated by provided argument +/// +/// ``` +/// # #[cfg(feature = "code-in-doc")] { +/// # use rinja::Template; +/// /// ```jinja +/// ///
{{ example|join(", ") }}
+/// /// ``` +/// #[derive(Template)] +/// #[template(ext = "html", in_doc = true)] +/// struct Example<'a> { +/// example: &'a [&'a str], +/// } +/// +/// assert_eq!( +/// Example { example: &["foo", "bar", "bazz"] }.to_string(), +/// "
foo, bar, bazz
" +/// ); +/// # } +/// ``` +#[inline] +pub fn join(input: I, separator: S) -> Result, Infallible> +where + I: IntoIterator, + I::Item: fmt::Display, + S: fmt::Display, +{ + Ok(JoinFilter(Cell::new(Some((input, separator))))) +} + +/// Result of the filter [`join()`]. +/// +/// ## Note +/// +/// This struct implements [`fmt::Display`], but only produces a string once. +/// Any subsequent call to `.to_string()` will result in an empty string, because the iterator is +/// already consumed. +// The filter contains a [`Cell`], so we can modify iterator inside a method that takes `self` by +// reference: [`fmt::Display::fmt()`] normally has the contract that it will produce the same result +// in multiple invocations for the same object. We break this contract, because have to consume the +// iterator, unless we want to enforce `I: Clone`, nor do we want to "memorize" the result of the +// joined data. +pub struct JoinFilter(Cell>); + +impl fmt::Display for JoinFilter +where + I: IntoIterator, + I::Item: fmt::Display, + S: fmt::Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Some((iter, separator)) = self.0.take() else { + return Ok(()); + }; + for (idx, token) in iter.into_iter().enumerate() { + match idx { + 0 => f.write_fmt(format_args!("{token}"))?, + _ => f.write_fmt(format_args!("{separator}{token}"))?, + } + } + Ok(()) + } +} + +/// Capitalize a value. The first character will be uppercase, all others lowercase. +/// +/// ``` +/// # #[cfg(feature = "code-in-doc")] { +/// # use rinja::Template; +/// /// ```jinja +/// ///
{{ example|capitalize }}
+/// /// ``` +/// #[derive(Template)] +/// #[template(ext = "html", in_doc = true)] +/// struct Example<'a> { +/// example: &'a str, +/// } +/// +/// assert_eq!( +/// Example { example: "hello" }.to_string(), +/// "
Hello
" +/// ); +/// +/// assert_eq!( +/// Example { example: "hElLO" }.to_string(), +/// "
Hello
" +/// ); +/// # } +/// ``` +#[inline] +pub fn capitalize(s: impl fmt::Display) -> Result { + fn capitalize(s: &str) -> Result { + let mut chars = s.chars(); + if let Some(c) = chars.next() { + let mut replacement = String::with_capacity(s.len()); + replacement.extend(c.to_uppercase()); + replacement.push_str(&chars.as_str().to_lowercase()); + Ok(replacement) + } else { + Ok(String::new()) + } + } + + let mut buffer; + capitalize(try_to_str!(s => buffer)) +} + +/// Centers the value in a field of a given width +/// +/// ``` +/// # #[cfg(feature = "code-in-doc")] { +/// # use rinja::Template; +/// /// ```jinja +/// ///
-{{ example|center(5) }}-
+/// /// ``` +/// #[derive(Template)] +/// #[template(ext = "html", in_doc = true)] +/// struct Example<'a> { +/// example: &'a str, +/// } +/// +/// assert_eq!( +/// Example { example: "a" }.to_string(), +/// "
- a -
" +/// ); +/// # } +/// ``` +#[inline] +pub fn center(src: T, width: usize) -> Result, Infallible> { + Ok(Center { src, width }) +} + +pub struct Center { + src: T, + width: usize, +} + +impl fmt::Display for Center { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.width < MAX_LEN { + write!(f, "{: ^1$}", self.src, self.width) + } else { + write!(f, "{}", self.src) + } + } +} + +/// Count the words in that string. +/// +/// ``` +/// # #[cfg(feature = "code-in-doc")] { +/// # use rinja::Template; +/// /// ```jinja +/// ///
{{ example|wordcount }}
+/// /// ``` +/// #[derive(Template)] +/// #[template(ext = "html", in_doc = true)] +/// struct Example<'a> { +/// example: &'a str, +/// } +/// +/// assert_eq!( +/// Example { example: "rinja is sort of cool" }.to_string(), +/// "
5
" +/// ); +/// # } +/// ``` +pub fn wordcount(s: impl fmt::Display) -> Result { + let mut buffer; + Ok(try_to_str!(s => buffer).split_whitespace().count()) +} + +/// Return a title cased version of the value. Words will start with uppercase letters, all +/// remaining characters are lowercase. +/// +/// ``` +/// # #[cfg(feature = "code-in-doc")] { +/// # use rinja::Template; +/// /// ```jinja +/// ///
{{ example|title }}
+/// /// ``` +/// #[derive(Template)] +/// #[template(ext = "html", in_doc = true)] +/// struct Example<'a> { +/// example: &'a str, +/// } +/// +/// assert_eq!( +/// Example { example: "hello WORLD" }.to_string(), +/// "
Hello World
" +/// ); +/// # } +/// ``` +pub fn title(s: impl fmt::Display) -> Result { + let mut buffer; + let s = try_to_str!(s => buffer); + let mut need_capitalization = true; + + // Sadly enough, we can't mutate a string when iterating over its chars, likely because it could + // change the size of a char, "breaking" the char indices. + let mut output = String::with_capacity(s.len()); + for c in s.chars() { + if c.is_whitespace() { + output.push(c); + need_capitalization = true; + } else if need_capitalization { + match c.is_uppercase() { + true => output.push(c), + false => output.extend(c.to_uppercase()), + } + need_capitalization = false; + } else { + match c.is_lowercase() { + true => output.push(c), + false => output.extend(c.to_lowercase()), + } + } + } + Ok(output) +} + +/// For a value of `±1` by default an empty string `""` is returned, otherwise `"s"`. +/// +/// # Examples +/// +/// ## With default arguments +/// +/// ``` +/// # #[cfg(feature = "code-in-doc")] { +/// # use rinja::Template; +/// /// ```jinja +/// /// I have {{dogs}} dog{{dogs|pluralize}} and {{cats}} cat{{cats|pluralize}}. +/// /// ``` +/// #[derive(Template)] +/// #[template(ext = "html", in_doc = true)] +/// struct Pets { +/// dogs: i8, +/// cats: i8, +/// } +/// +/// assert_eq!( +/// Pets { dogs: 0, cats: 0 }.to_string(), +/// "I have 0 dogs and 0 cats." +/// ); +/// assert_eq!( +/// Pets { dogs: 1, cats: 1 }.to_string(), +/// "I have 1 dog and 1 cat." +/// ); +/// assert_eq!( +/// Pets { dogs: -1, cats: 99 }.to_string(), +/// "I have -1 dog and 99 cats." +/// ); +/// # } +/// ``` +/// +/// ## Overriding the singular case +/// +/// ``` +/// # #[cfg(feature = "code-in-doc")] { +/// # use rinja::Template; +/// /// ```jinja +/// /// I have {{dogs}} dog{{ dogs|pluralize("go") }}. +/// /// ``` +/// #[derive(Template)] +/// #[template(ext = "html", in_doc = true)] +/// struct Dog { +/// dogs: i8, +/// } +/// +/// assert_eq!( +/// Dog { dogs: 0 }.to_string(), +/// "I have 0 dogs." +/// ); +/// assert_eq!( +/// Dog { dogs: 1 }.to_string(), +/// "I have 1 doggo." +/// ); +/// # } +/// ``` +/// +/// ## Overriding singular and plural cases +/// +/// ``` +/// # #[cfg(feature = "code-in-doc")] { +/// # use rinja::Template; +/// /// ```jinja +/// /// I have {{mice}} {{ mice|pluralize("mouse", "mice") }}. +/// /// ``` +/// #[derive(Template)] +/// #[template(ext = "html", in_doc = true)] +/// struct Mice { +/// mice: i8, +/// } +/// +/// assert_eq!( +/// Mice { mice: 42 }.to_string(), +/// "I have 42 mice." +/// ); +/// assert_eq!( +/// Mice { mice: 1 }.to_string(), +/// "I have 1 mouse." +/// ); +/// # } +/// ``` +/// +/// ## Arguments get escaped +/// +/// ``` +/// # #[cfg(feature = "code-in-doc")] { +/// # use rinja::Template; +/// /// ```jinja +/// /// You are number {{ number|pluralize("ONE", number) }}! +/// /// ``` +/// #[derive(Template)] +/// #[template(ext = "html", in_doc = true)] +/// struct Number { +/// number: usize +/// } +/// +/// assert_eq!( +/// Number { number: 1 }.to_string(), +/// "You are number <b>ONE</b>!", +/// ); +/// assert_eq!( +/// Number { number: 9000 }.to_string(), +/// "You are number 9000!", +/// ); +/// # } +/// ``` +#[inline] +pub fn pluralize(count: C, singular: S, plural: P) -> Result, C::Error> +where + C: PluralizeCount, +{ + match count.is_singular()? { + true => Ok(Pluralize::Singular(singular)), + false => Ok(Pluralize::Plural(plural)), + } +} + +/// An integer that can have the value `+1` and maybe `-1`. +pub trait PluralizeCount { + /// A possible error that can occur while checking the value. + type Error: Into; + + /// Returns `true` if and only if the value is `±1`. + fn is_singular(&self) -> Result; +} + +const _: () = { + crate::impl_for_ref! { + impl PluralizeCount for T { + type Error = T::Error; + + #[inline] + fn is_singular(&self) -> Result { + ::is_singular(self) + } + } + } + + impl PluralizeCount for Pin + where + T: Deref, + ::Target: PluralizeCount, + { + type Error = <::Target as PluralizeCount>::Error; + + #[inline] + fn is_singular(&self) -> Result { + self.as_ref().get_ref().is_singular() + } + } + + /// implement `PluralizeCount` for unsigned integer types + macro_rules! impl_pluralize_for_unsigned_int { + ($($ty:ty)*) => { $( + impl PluralizeCount for $ty { + type Error = Infallible; + + #[inline] + fn is_singular(&self) -> Result { + Ok(*self == 1) + } + } + )* }; + } + + impl_pluralize_for_unsigned_int!(u8 u16 u32 u64 u128 usize); + + /// implement `PluralizeCount` for signed integer types + macro_rules! impl_pluralize_for_signed_int { + ($($ty:ty)*) => { $( + impl PluralizeCount for $ty { + type Error = Infallible; + + #[inline] + fn is_singular(&self) -> Result { + Ok(*self == 1 || *self == -1) + } + } + )* }; + } + + impl_pluralize_for_signed_int!(i8 i16 i32 i64 i128 isize); + + /// implement `PluralizeCount` for non-zero integer types + macro_rules! impl_pluralize_for_non_zero { + ($($ty:ident)*) => { $( + impl PluralizeCount for std::num::$ty { + type Error = Infallible; + + #[inline] + fn is_singular(&self) -> Result { + self.get().is_singular() + } + } + )* }; + } + + impl_pluralize_for_non_zero! { + NonZeroI8 NonZeroI16 NonZeroI32 NonZeroI64 NonZeroI128 NonZeroIsize + NonZeroU8 NonZeroU16 NonZeroU32 NonZeroU64 NonZeroU128 NonZeroUsize + } +}; + +pub enum Pluralize { + Singular(S), + Plural(P), +} + +impl fmt::Display for Pluralize { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Pluralize::Singular(value) => write!(f, "{value}"), + Pluralize::Plural(value) => write!(f, "{value}"), + } + } +} + +impl FastWritable for Pluralize { + #[inline] + fn write_into(&self, dest: &mut W) -> crate::Result<()> { + match self { + Pluralize::Singular(value) => value.write_into(dest), + Pluralize::Plural(value) => value.write_into(dest), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_linebreaks() { + assert_eq!( + linebreaks("Foo\nBar Baz").unwrap().to_string(), + "

Foo
Bar Baz

" + ); + assert_eq!( + linebreaks("Foo\nBar\n\nBaz").unwrap().to_string(), + "

Foo
Bar

Baz

" + ); + } + + #[test] + fn test_linebreaksbr() { + assert_eq!(linebreaksbr("Foo\nBar").unwrap().to_string(), "Foo
Bar"); + assert_eq!( + linebreaksbr("Foo\nBar\n\nBaz").unwrap().to_string(), + "Foo
Bar

Baz" + ); + } + + #[test] + fn test_paragraphbreaks() { + assert_eq!( + paragraphbreaks("Foo\nBar Baz").unwrap().to_string(), + "

Foo\nBar Baz

" + ); + assert_eq!( + paragraphbreaks("Foo\nBar\n\nBaz").unwrap().to_string(), + "

Foo\nBar

Baz

" + ); + assert_eq!( + paragraphbreaks("Foo\n\n\n\n\nBar\n\nBaz") + .unwrap() + .to_string(), + "

Foo

\nBar

Baz

" + ); + } + + #[test] + fn test_lower() { + assert_eq!(lower("Foo").unwrap().to_string(), "foo"); + assert_eq!(lower("FOO").unwrap().to_string(), "foo"); + assert_eq!(lower("FooBar").unwrap().to_string(), "foobar"); + assert_eq!(lower("foo").unwrap().to_string(), "foo"); + } + + #[test] + fn test_upper() { + assert_eq!(upper("Foo").unwrap().to_string(), "FOO"); + assert_eq!(upper("FOO").unwrap().to_string(), "FOO"); + assert_eq!(upper("FooBar").unwrap().to_string(), "FOOBAR"); + assert_eq!(upper("foo").unwrap().to_string(), "FOO"); + } + + #[test] + fn test_trim() { + assert_eq!(trim(" Hello\tworld\t").unwrap().to_string(), "Hello\tworld"); + } + + #[test] + fn test_truncate() { + assert_eq!(truncate("hello", 2).unwrap().to_string(), "he..."); + let a = String::from("您好"); + assert_eq!(a.len(), 6); + assert_eq!(String::from("您").len(), 3); + assert_eq!(truncate("您好", 1).unwrap().to_string(), "您..."); + assert_eq!(truncate("您好", 2).unwrap().to_string(), "您..."); + assert_eq!(truncate("您好", 3).unwrap().to_string(), "您..."); + assert_eq!(truncate("您好", 4).unwrap().to_string(), "您好"); + assert_eq!(truncate("您好", 5).unwrap().to_string(), "您好"); + assert_eq!(truncate("您好", 6).unwrap().to_string(), "您好"); + assert_eq!(truncate("您好", 7).unwrap().to_string(), "您好"); + let s = String::from("🤚a🤚"); + assert_eq!(s.len(), 9); + assert_eq!(String::from("🤚").len(), 4); + assert_eq!(truncate("🤚a🤚", 1).unwrap().to_string(), "🤚..."); + assert_eq!(truncate("🤚a🤚", 2).unwrap().to_string(), "🤚..."); + assert_eq!(truncate("🤚a🤚", 3).unwrap().to_string(), "🤚..."); + assert_eq!(truncate("🤚a🤚", 4).unwrap().to_string(), "🤚..."); + assert_eq!(truncate("🤚a🤚", 5).unwrap().to_string(), "🤚a..."); + assert_eq!(truncate("🤚a🤚", 6).unwrap().to_string(), "🤚a🤚"); + assert_eq!(truncate("🤚a🤚", 6).unwrap().to_string(), "🤚a🤚"); + assert_eq!(truncate("🤚a🤚", 7).unwrap().to_string(), "🤚a🤚"); + assert_eq!(truncate("🤚a🤚", 8).unwrap().to_string(), "🤚a🤚"); + assert_eq!(truncate("🤚a🤚", 9).unwrap().to_string(), "🤚a🤚"); + assert_eq!(truncate("🤚a🤚", 10).unwrap().to_string(), "🤚a🤚"); + } + + #[test] + fn test_indent() { + assert_eq!(indent("hello", 2).unwrap().to_string(), "hello"); + assert_eq!(indent("hello\n", 2).unwrap().to_string(), "hello\n"); + assert_eq!(indent("hello\nfoo", 2).unwrap().to_string(), "hello\n foo"); + assert_eq!( + indent("hello\nfoo\n bar", 4).unwrap().to_string(), + "hello\n foo\n bar" + ); + assert_eq!( + indent("hello", 267_332_238_858).unwrap().to_string(), + "hello" + ); + } + + #[allow(clippy::needless_borrow)] + #[test] + fn test_join() { + assert_eq!( + join((&["hello", "world"]).iter(), ", ") + .unwrap() + .to_string(), + "hello, world" + ); + assert_eq!( + join((&["hello"]).iter(), ", ").unwrap().to_string(), + "hello" + ); + + let empty: &[&str] = &[]; + assert_eq!(join(empty.iter(), ", ").unwrap().to_string(), ""); + + let input: Vec = vec!["foo".into(), "bar".into(), "bazz".into()]; + assert_eq!(join(input.iter(), ":").unwrap().to_string(), "foo:bar:bazz"); + + let input: &[String] = &["foo".into(), "bar".into()]; + assert_eq!(join(input.iter(), ":").unwrap().to_string(), "foo:bar"); + + let real: String = "blah".into(); + let input: Vec<&str> = vec![&real]; + assert_eq!(join(input.iter(), ";").unwrap().to_string(), "blah"); + + assert_eq!( + join((&&&&&["foo", "bar"]).iter(), ", ") + .unwrap() + .to_string(), + "foo, bar" + ); + } + + #[test] + fn test_capitalize() { + assert_eq!(capitalize("foo").unwrap().to_string(), "Foo".to_string()); + assert_eq!(capitalize("f").unwrap().to_string(), "F".to_string()); + assert_eq!(capitalize("fO").unwrap().to_string(), "Fo".to_string()); + assert_eq!(capitalize("").unwrap().to_string(), String::new()); + assert_eq!(capitalize("FoO").unwrap().to_string(), "Foo".to_string()); + assert_eq!( + capitalize("foO BAR").unwrap().to_string(), + "Foo bar".to_string() + ); + assert_eq!( + capitalize("äØÄÅÖ").unwrap().to_string(), + "Äøäåö".to_string() + ); + assert_eq!(capitalize("ß").unwrap().to_string(), "SS".to_string()); + assert_eq!(capitalize("ßß").unwrap().to_string(), "SSß".to_string()); + } + + #[test] + fn test_center() { + assert_eq!(center("f", 3).unwrap().to_string(), " f ".to_string()); + assert_eq!(center("f", 4).unwrap().to_string(), " f ".to_string()); + assert_eq!(center("foo", 1).unwrap().to_string(), "foo".to_string()); + assert_eq!( + center("foo bar", 8).unwrap().to_string(), + "foo bar ".to_string() + ); + assert_eq!( + center("foo", 111_669_149_696).unwrap().to_string(), + "foo".to_string() + ); + } + + #[test] + fn test_wordcount() { + assert_eq!(wordcount("").unwrap(), 0); + assert_eq!(wordcount(" \n\t").unwrap(), 0); + assert_eq!(wordcount("foo").unwrap(), 1); + assert_eq!(wordcount("foo bar").unwrap(), 2); + assert_eq!(wordcount("foo bar").unwrap(), 2); + } + + #[test] + fn test_title() { + assert_eq!(&title("").unwrap(), ""); + assert_eq!(&title(" \n\t").unwrap(), " \n\t"); + assert_eq!(&title("foo").unwrap(), "Foo"); + assert_eq!(&title(" foo").unwrap(), " Foo"); + assert_eq!(&title("foo bar").unwrap(), "Foo Bar"); + assert_eq!(&title("foo bar ").unwrap(), "Foo Bar "); + assert_eq!(&title("fOO").unwrap(), "Foo"); + assert_eq!(&title("fOo BaR").unwrap(), "Foo Bar"); + } + + #[test] + fn fuzzed_indent_filter() { + let s = "hello\nfoo\nbar".to_string().repeat(1024); + assert_eq!(indent(s.clone(), 4).unwrap().to_string(), s); + } +}