mirror of
https://github.com/askama-rs/askama.git
synced 2025-09-28 05:21:14 +00:00
Make #![no_std]
compatible
This commit is contained in:
parent
f00e5e2091
commit
bedc31797c
2
.github/workflows/rust.yml
vendored
2
.github/workflows/rust.yml
vendored
@ -91,7 +91,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: "1.80.0"
|
||||
toolchain: "1.81.0"
|
||||
- run: cargo check --lib -p rinja --all-features
|
||||
|
||||
Audit:
|
||||
|
@ -11,7 +11,7 @@ license = "MIT OR Apache-2.0"
|
||||
workspace = ".."
|
||||
readme = "../README.md"
|
||||
edition = "2021"
|
||||
rust-version = "1.80"
|
||||
rust-version = "1.81"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["full"]
|
||||
@ -21,20 +21,21 @@ rustdoc-args = ["--generate-link-to-definition", "--cfg=docsrs"]
|
||||
maintenance = { status = "actively-developed" }
|
||||
|
||||
[features]
|
||||
default = ["config", "urlencode"]
|
||||
default = ["config", "std", "urlencode"]
|
||||
alloc = ["rinja_derive/alloc", "serde?/alloc", "serde_json?/alloc", "percent-encoding?/alloc"]
|
||||
full = ["default", "code-in-doc", "serde_json"]
|
||||
code-in-doc = ["rinja_derive/code-in-doc"]
|
||||
config = ["rinja_derive/config"]
|
||||
serde_json = ["rinja_derive/serde_json", "dep:serde", "dep:serde_json"]
|
||||
std = ["alloc", "rinja_derive/std", "serde?/std", "serde_json?/std", "percent-encoding?/std"]
|
||||
urlencode = ["rinja_derive/urlencode", "dep:percent-encoding"]
|
||||
|
||||
[dependencies]
|
||||
rinja_derive = { version = "=0.3.5", path = "../rinja_derive" }
|
||||
|
||||
num-traits = { version = "0.2.6", optional = true }
|
||||
percent-encoding = { version = "2.1.0", optional = true }
|
||||
serde = { version = "1.0", optional = true }
|
||||
serde_json = { version = "1.0", optional = true }
|
||||
percent-encoding = { version = "2.1.0", optional = true, default-features = false }
|
||||
serde = { version = "1.0", optional = true, default-features = false }
|
||||
serde_json = { version = "1.0", optional = true, default-features = false, features = [] }
|
||||
|
||||
itoa = "1.0.11"
|
||||
|
||||
|
@ -1,9 +1,13 @@
|
||||
use std::convert::Infallible;
|
||||
use std::error::Error as StdError;
|
||||
use std::{fmt, io};
|
||||
#[cfg(feature = "alloc")]
|
||||
use alloc::boxed::Box;
|
||||
use core::convert::Infallible;
|
||||
use core::error::Error as StdError;
|
||||
use core::fmt;
|
||||
#[cfg(feature = "std")]
|
||||
use std::io;
|
||||
|
||||
/// The [`Result`](std::result::Result) type with [`Error`] as default error type
|
||||
pub type Result<I, E = Error> = std::result::Result<I, E>;
|
||||
pub type Result<I, E = Error> = core::result::Result<I, E>;
|
||||
|
||||
/// rinja's error type
|
||||
///
|
||||
@ -15,6 +19,7 @@ pub enum Error {
|
||||
/// Generic, unspecified formatting error
|
||||
Fmt,
|
||||
/// An error raised by using `?` in a template
|
||||
#[cfg(feature = "alloc")]
|
||||
Custom(Box<dyn StdError + Send + Sync>),
|
||||
/// JSON conversion error
|
||||
#[cfg(feature = "serde_json")]
|
||||
@ -24,12 +29,14 @@ pub enum Error {
|
||||
impl Error {
|
||||
/// Capture an [`StdError`]
|
||||
#[inline]
|
||||
#[cfg(feature = "alloc")]
|
||||
pub fn custom(err: impl Into<Box<dyn StdError + Send + Sync>>) -> Self {
|
||||
Self::Custom(err.into())
|
||||
}
|
||||
|
||||
/// Convert this [`Error`] into a
|
||||
/// <code>[Box]<dyn [StdError] + [Send] + [Sync]></code>
|
||||
#[cfg(feature = "alloc")]
|
||||
pub fn into_box(self) -> Box<dyn StdError + Send + Sync> {
|
||||
match self {
|
||||
Error::Fmt => fmt::Error.into(),
|
||||
@ -42,6 +49,7 @@ impl Error {
|
||||
/// Convert this [`Error`] into an [`io::Error`]
|
||||
///
|
||||
/// Not this error itself, but the contained [`source`][StdError::source] is returned.
|
||||
#[cfg(feature = "std")]
|
||||
pub fn into_io_error(self) -> io::Error {
|
||||
io::Error::other(match self {
|
||||
Error::Custom(err) => match err.downcast() {
|
||||
@ -57,6 +65,7 @@ impl StdError for Error {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
match self {
|
||||
Error::Fmt => Some(&fmt::Error),
|
||||
#[cfg(feature = "alloc")]
|
||||
Error::Custom(err) => Some(err.as_ref()),
|
||||
#[cfg(feature = "serde_json")]
|
||||
Error::Json(err) => Some(err),
|
||||
@ -68,6 +77,7 @@ impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Error::Fmt => fmt::Error.fmt(f),
|
||||
#[cfg(feature = "alloc")]
|
||||
Error::Custom(err) => err.fmt(f),
|
||||
#[cfg(feature = "serde_json")]
|
||||
Error::Json(err) => err.fmt(f),
|
||||
@ -82,6 +92,7 @@ impl From<Error> for fmt::Error {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl From<Error> for io::Error {
|
||||
#[inline]
|
||||
fn from(err: Error) -> Self {
|
||||
@ -97,6 +108,7 @@ impl From<fmt::Error> for Error {
|
||||
}
|
||||
|
||||
/// This conversion inspects the argument and chooses the best fitting [`Error`] variant
|
||||
#[cfg(feature = "alloc")]
|
||||
impl From<Box<dyn StdError + Send + Sync>> for Error {
|
||||
#[inline]
|
||||
fn from(err: Box<dyn StdError + Send + Sync>) -> Self {
|
||||
@ -105,6 +117,7 @@ impl From<Box<dyn StdError + Send + Sync>> for Error {
|
||||
}
|
||||
|
||||
/// This conversion inspects the argument and chooses the best fitting [`Error`] variant
|
||||
#[cfg(feature = "std")]
|
||||
impl From<io::Error> for Error {
|
||||
#[inline]
|
||||
fn from(err: io::Error) -> Self {
|
||||
@ -112,12 +125,16 @@ impl From<io::Error> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
const MAX_ERROR_UNWRAP_COUNT: usize = 5;
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
fn error_from_stderror(err: Box<dyn StdError + Send + Sync>, unwraps: usize) -> Error {
|
||||
let Some(unwraps) = unwraps.checked_sub(1) else {
|
||||
return Error::Custom(err);
|
||||
};
|
||||
#[cfg(not(feature = "std"))]
|
||||
let _ = unwraps;
|
||||
match ErrorKind::inspect(err.as_ref()) {
|
||||
ErrorKind::Fmt => Error::Fmt,
|
||||
ErrorKind::Custom => Error::Custom(err),
|
||||
@ -126,6 +143,7 @@ fn error_from_stderror(err: Box<dyn StdError + Send + Sync>, unwraps: usize) ->
|
||||
Ok(err) => Error::Json(*err),
|
||||
Err(_) => Error::Fmt, // unreachable
|
||||
},
|
||||
#[cfg(feature = "std")]
|
||||
ErrorKind::Io => match err.downcast() {
|
||||
Ok(err) => from_from_io_error(*err, unwraps),
|
||||
Err(_) => Error::Fmt, // unreachable
|
||||
@ -137,6 +155,7 @@ fn error_from_stderror(err: Box<dyn StdError + Send + Sync>, unwraps: usize) ->
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
fn from_from_io_error(err: io::Error, unwraps: usize) -> Error {
|
||||
let Some(inner) = err.get_ref() else {
|
||||
return Error::custom(err);
|
||||
@ -169,30 +188,39 @@ fn from_from_io_error(err: io::Error, unwraps: usize) -> Error {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
enum ErrorKind {
|
||||
Fmt,
|
||||
Custom,
|
||||
#[cfg(feature = "serde_json")]
|
||||
Json,
|
||||
#[cfg(feature = "std")]
|
||||
Io,
|
||||
Rinja,
|
||||
}
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
impl ErrorKind {
|
||||
fn inspect(err: &(dyn StdError + 'static)) -> ErrorKind {
|
||||
if err.is::<fmt::Error>() {
|
||||
ErrorKind::Fmt
|
||||
} else if err.is::<io::Error>() {
|
||||
ErrorKind::Io
|
||||
} else if err.is::<Error>() {
|
||||
ErrorKind::Rinja
|
||||
} else {
|
||||
#[cfg(feature = "serde_json")]
|
||||
if err.is::<serde_json::Error>() {
|
||||
return ErrorKind::Json;
|
||||
}
|
||||
ErrorKind::Custom
|
||||
return ErrorKind::Fmt;
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
if err.is::<io::Error>() {
|
||||
return ErrorKind::Io;
|
||||
}
|
||||
|
||||
if err.is::<Error>() {
|
||||
return ErrorKind::Rinja;
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde_json")]
|
||||
if err.is::<serde_json::Error>() {
|
||||
return ErrorKind::Json;
|
||||
}
|
||||
|
||||
ErrorKind::Custom
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,9 @@
|
||||
use std::cell::Cell;
|
||||
use std::convert::Infallible;
|
||||
use std::fmt::{self, Write};
|
||||
use std::ops::Deref;
|
||||
use std::pin::Pin;
|
||||
use alloc::string::String;
|
||||
use core::fmt::{self, Write};
|
||||
|
||||
use super::escape::{FastWritable, HtmlSafeOutput};
|
||||
use crate::{Error, Result};
|
||||
|
||||
// MAX_LEN is maximum allowed length for filters.
|
||||
const MAX_LEN: usize = 10_000;
|
||||
use super::MAX_LEN;
|
||||
use super::escape::HtmlSafeOutput;
|
||||
use crate::Result;
|
||||
|
||||
/// Return an ephemeral `&str` for `$src: impl fmt::Display`
|
||||
///
|
||||
@ -126,7 +121,7 @@ pub fn format() {}
|
||||
pub fn linebreaks(s: impl fmt::Display) -> Result<HtmlSafeOutput<String>, fmt::Error> {
|
||||
fn linebreaks(s: &str) -> String {
|
||||
let linebroken = s.replace("\n\n", "</p><p>").replace('\n', "<br/>");
|
||||
format!("<p>{linebroken}</p>")
|
||||
alloc::format!("<p>{linebroken}</p>")
|
||||
}
|
||||
|
||||
let mut buffer;
|
||||
@ -191,7 +186,7 @@ pub fn linebreaksbr(s: impl fmt::Display) -> Result<HtmlSafeOutput<String>, fmt:
|
||||
pub fn paragraphbreaks(s: impl fmt::Display) -> Result<HtmlSafeOutput<String>, fmt::Error> {
|
||||
fn paragraphbreaks(s: &str) -> String {
|
||||
let linebroken = s.replace("\n\n", "</p><p>").replace("<p></p>", "");
|
||||
format!("<p>{linebroken}</p>")
|
||||
alloc::format!("<p>{linebroken}</p>")
|
||||
}
|
||||
|
||||
let mut buffer;
|
||||
@ -340,6 +335,7 @@ pub fn uppercase(s: impl fmt::Display) -> Result<String, fmt::Error> {
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
#[cfg(feature = "alloc")]
|
||||
pub fn trim<T: fmt::Display>(s: T) -> Result<String> {
|
||||
struct Collector(String);
|
||||
|
||||
@ -359,112 +355,6 @@ pub fn trim<T: fmt::Display>(s: T) -> Result<String> {
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
/// Limit string length, appends '...' if truncated
|
||||
///
|
||||
/// ```
|
||||
/// # #[cfg(feature = "code-in-doc")] {
|
||||
/// # use rinja::Template;
|
||||
/// /// ```jinja
|
||||
/// /// <div>{{ example|truncate(2) }}</div>
|
||||
/// /// ```
|
||||
/// #[derive(Template)]
|
||||
/// #[template(ext = "html", in_doc = true)]
|
||||
/// struct Example<'a> {
|
||||
/// example: &'a str,
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// Example { example: "hello" }.to_string(),
|
||||
/// "<div>he...</div>"
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn truncate<S: fmt::Display>(
|
||||
source: S,
|
||||
remaining: usize,
|
||||
) -> Result<TruncateFilter<S>, Infallible> {
|
||||
Ok(TruncateFilter { source, remaining })
|
||||
}
|
||||
|
||||
pub struct TruncateFilter<S> {
|
||||
source: S,
|
||||
remaining: usize,
|
||||
}
|
||||
|
||||
impl<S: fmt::Display> fmt::Display for TruncateFilter<S> {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(TruncateWriter::new(f, self.remaining), "{}", self.source)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: FastWritable> FastWritable for TruncateFilter<S> {
|
||||
#[inline]
|
||||
fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> crate::Result<()> {
|
||||
self.source
|
||||
.write_into(&mut TruncateWriter::new(dest, self.remaining))
|
||||
}
|
||||
}
|
||||
|
||||
struct TruncateWriter<W> {
|
||||
dest: Option<W>,
|
||||
remaining: usize,
|
||||
}
|
||||
|
||||
impl<W> TruncateWriter<W> {
|
||||
fn new(dest: W, remaining: usize) -> Self {
|
||||
TruncateWriter {
|
||||
dest: Some(dest),
|
||||
remaining,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: fmt::Write> fmt::Write for TruncateWriter<W> {
|
||||
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
|
||||
///
|
||||
/// ```
|
||||
@ -494,7 +384,7 @@ pub fn indent(s: impl fmt::Display, width: usize) -> Result<String, fmt::Error>
|
||||
return Ok(buffer);
|
||||
} else if let Some(s) = args.as_str() {
|
||||
if s.len() >= MAX_LEN {
|
||||
return Ok(s.to_owned());
|
||||
return Ok(s.into());
|
||||
} else {
|
||||
s
|
||||
}
|
||||
@ -521,70 +411,6 @@ pub fn indent(s: impl fmt::Display, width: usize) -> Result<String, fmt::Error>
|
||||
indent(format_args!("{s}"), width)
|
||||
}
|
||||
|
||||
/// Joins iterable into a string separated by provided argument
|
||||
///
|
||||
/// ```
|
||||
/// # #[cfg(feature = "code-in-doc")] {
|
||||
/// # use rinja::Template;
|
||||
/// /// ```jinja
|
||||
/// /// <div>{{ example|join(", ") }}</div>
|
||||
/// /// ```
|
||||
/// #[derive(Template)]
|
||||
/// #[template(ext = "html", in_doc = true)]
|
||||
/// struct Example<'a> {
|
||||
/// example: &'a [&'a str],
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// Example { example: &["foo", "bar", "bazz"] }.to_string(),
|
||||
/// "<div>foo, bar, bazz</div>"
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn join<I, S>(input: I, separator: S) -> Result<JoinFilter<I, S>, 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<I, S>(Cell<Option<(I, S)>>);
|
||||
|
||||
impl<I, S> fmt::Display for JoinFilter<I, S>
|
||||
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.
|
||||
///
|
||||
/// ```
|
||||
@ -628,46 +454,6 @@ pub fn capitalize(s: impl fmt::Display) -> Result<String, fmt::Error> {
|
||||
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
|
||||
/// /// <div>-{{ example|center(5) }}-</div>
|
||||
/// /// ```
|
||||
/// #[derive(Template)]
|
||||
/// #[template(ext = "html", in_doc = true)]
|
||||
/// struct Example<'a> {
|
||||
/// example: &'a str,
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// Example { example: "a" }.to_string(),
|
||||
/// "<div>- a -</div>"
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn center<T: fmt::Display>(src: T, width: usize) -> Result<Center<T>, Infallible> {
|
||||
Ok(Center { src, width })
|
||||
}
|
||||
|
||||
pub struct Center<T> {
|
||||
src: T,
|
||||
width: usize,
|
||||
}
|
||||
|
||||
impl<T: fmt::Display> fmt::Display for Center<T> {
|
||||
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.
|
||||
///
|
||||
/// ```
|
||||
@ -742,238 +528,10 @@ pub fn title(s: impl fmt::Display) -> Result<String, fmt::Error> {
|
||||
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("<b>ONE</b>", 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<C, S, P>(count: C, singular: S, plural: P) -> Result<Pluralize<S, P>, 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<Error>;
|
||||
|
||||
/// Returns `true` if and only if the value is `±1`.
|
||||
fn is_singular(&self) -> Result<bool, Self::Error>;
|
||||
}
|
||||
|
||||
const _: () = {
|
||||
crate::impl_for_ref! {
|
||||
impl PluralizeCount for T {
|
||||
type Error = T::Error;
|
||||
|
||||
#[inline]
|
||||
fn is_singular(&self) -> Result<bool, Self::Error> {
|
||||
<T>::is_singular(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> PluralizeCount for Pin<T>
|
||||
where
|
||||
T: Deref,
|
||||
<T as Deref>::Target: PluralizeCount,
|
||||
{
|
||||
type Error = <<T as Deref>::Target as PluralizeCount>::Error;
|
||||
|
||||
#[inline]
|
||||
fn is_singular(&self) -> Result<bool, Self::Error> {
|
||||
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<bool, Self::Error> {
|
||||
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<bool, Self::Error> {
|
||||
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<bool, Self::Error> {
|
||||
self.get().is_singular()
|
||||
}
|
||||
}
|
||||
)* };
|
||||
}
|
||||
|
||||
impl_pluralize_for_non_zero! {
|
||||
NonZeroI8 NonZeroI16 NonZeroI32 NonZeroI64 NonZeroI128 NonZeroIsize
|
||||
NonZeroU8 NonZeroU16 NonZeroU32 NonZeroU64 NonZeroU128 NonZeroUsize
|
||||
}
|
||||
};
|
||||
|
||||
pub enum Pluralize<S, P> {
|
||||
Singular(S),
|
||||
Plural(P),
|
||||
}
|
||||
|
||||
impl<S: fmt::Display, P: fmt::Display> fmt::Display for Pluralize<S, P> {
|
||||
#[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<S: FastWritable, P: FastWritable> FastWritable for Pluralize<S, P> {
|
||||
#[inline]
|
||||
fn write_into<W: fmt::Write + ?Sized>(&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 alloc::string::ToString;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
@ -1036,35 +594,6 @@ mod tests {
|
||||
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");
|
||||
@ -1080,41 +609,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[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<String> = 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());
|
||||
@ -1134,21 +628,6 @@ mod tests {
|
||||
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);
|
||||
|
@ -1,364 +1,13 @@
|
||||
use std::cell::Cell;
|
||||
use std::convert::Infallible;
|
||||
use std::fmt::{self, Write};
|
||||
use std::ops::Deref;
|
||||
use std::pin::Pin;
|
||||
use core::cell::Cell;
|
||||
use core::convert::Infallible;
|
||||
use core::fmt::{self, Write};
|
||||
use core::ops::Deref;
|
||||
use core::pin::Pin;
|
||||
|
||||
use super::escape::{FastWritable, HtmlSafeOutput};
|
||||
use super::MAX_LEN;
|
||||
use super::escape::FastWritable;
|
||||
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
|
||||
/// /// <div>{{ value|fmt("{:?}") }}</div>
|
||||
/// /// ```
|
||||
/// #[derive(Template)]
|
||||
/// #[template(ext = "html", in_doc = true)]
|
||||
/// struct Example {
|
||||
/// value: (usize, usize),
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// Example { value: (3, 4) }.to_string(),
|
||||
/// "<div>(3, 4)</div>"
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// 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
|
||||
/// /// <div>{{ "{:?}"|format(value) }}</div>
|
||||
/// /// ```
|
||||
/// #[derive(Template)]
|
||||
/// #[template(ext = "html", in_doc = true)]
|
||||
/// struct Example {
|
||||
/// value: (usize, usize),
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// Example { value: (3, 4) }.to_string(),
|
||||
/// "<div>(3, 4)</div>"
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// 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 `<br>` and a new line
|
||||
/// followed by a blank line becomes a paragraph break `<p>`.
|
||||
///
|
||||
/// ```
|
||||
/// # #[cfg(feature = "code-in-doc")] {
|
||||
/// # use rinja::Template;
|
||||
/// /// ```jinja
|
||||
/// /// <div>{{ example|linebreaks }}</div>
|
||||
/// /// ```
|
||||
/// #[derive(Template)]
|
||||
/// #[template(ext = "html", in_doc = true)]
|
||||
/// struct Example<'a> {
|
||||
/// example: &'a str,
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// Example { example: "Foo\nBar\n\nBaz" }.to_string(),
|
||||
/// "<div><p>Foo<br/>Bar</p><p>Baz</p></div>"
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn linebreaks(s: impl fmt::Display) -> Result<HtmlSafeOutput<String>, fmt::Error> {
|
||||
fn linebreaks(s: &str) -> String {
|
||||
let linebroken = s.replace("\n\n", "</p><p>").replace('\n', "<br/>");
|
||||
format!("<p>{linebroken}</p>")
|
||||
}
|
||||
|
||||
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
|
||||
/// /// <div>{{ lines|linebreaksbr }}</div>
|
||||
/// /// ```
|
||||
/// #[derive(Template)]
|
||||
/// #[template(ext = "html", in_doc = true)]
|
||||
/// struct Example<'a> {
|
||||
/// lines: &'a str,
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// Example { lines: "a\nb\nc" }.to_string(),
|
||||
/// "<div>a<br/>b<br/>c</div>"
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn linebreaksbr(s: impl fmt::Display) -> Result<HtmlSafeOutput<String>, fmt::Error> {
|
||||
fn linebreaksbr(s: &str) -> String {
|
||||
s.replace('\n', "<br/>")
|
||||
}
|
||||
|
||||
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 `<p>`.
|
||||
/// Paragraph tags only wrap content; empty paragraphs are removed.
|
||||
/// No `<br/>` 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(),
|
||||
/// "<p>Foo\nBar</p><p>Baz</p>"
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn paragraphbreaks(s: impl fmt::Display) -> Result<HtmlSafeOutput<String>, fmt::Error> {
|
||||
fn paragraphbreaks(s: &str) -> String {
|
||||
let linebroken = s.replace("\n\n", "</p><p>").replace("<p></p>", "");
|
||||
format!("<p>{linebroken}</p>")
|
||||
}
|
||||
|
||||
let mut buffer;
|
||||
Ok(HtmlSafeOutput(paragraphbreaks(try_to_str!(s => buffer))))
|
||||
}
|
||||
|
||||
/// Converts to lowercase
|
||||
///
|
||||
/// ```
|
||||
/// # #[cfg(feature = "code-in-doc")] {
|
||||
/// # use rinja::Template;
|
||||
/// /// ```jinja
|
||||
/// /// <div>{{ word|lower }}</div>
|
||||
/// /// ```
|
||||
/// #[derive(Template)]
|
||||
/// #[template(ext = "html", in_doc = true)]
|
||||
/// struct Example<'a> {
|
||||
/// word: &'a str,
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// Example { word: "FOO" }.to_string(),
|
||||
/// "<div>foo</div>"
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// Example { word: "FooBar" }.to_string(),
|
||||
/// "<div>foobar</div>"
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn lower(s: impl fmt::Display) -> Result<String, fmt::Error> {
|
||||
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
|
||||
/// /// <div>{{ word|lowercase }}</div>
|
||||
/// /// ```
|
||||
/// #[derive(Template)]
|
||||
/// #[template(ext = "html", in_doc = true)]
|
||||
/// struct Example<'a> {
|
||||
/// word: &'a str,
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// Example { word: "FOO" }.to_string(),
|
||||
/// "<div>foo</div>"
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// Example { word: "FooBar" }.to_string(),
|
||||
/// "<div>foobar</div>"
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn lowercase(s: impl fmt::Display) -> Result<String, fmt::Error> {
|
||||
lower(s)
|
||||
}
|
||||
|
||||
/// Converts to uppercase
|
||||
///
|
||||
/// ```
|
||||
/// # #[cfg(feature = "code-in-doc")] {
|
||||
/// # use rinja::Template;
|
||||
/// /// ```jinja
|
||||
/// /// <div>{{ word|upper }}</div>
|
||||
/// /// ```
|
||||
/// #[derive(Template)]
|
||||
/// #[template(ext = "html", in_doc = true)]
|
||||
/// struct Example<'a> {
|
||||
/// word: &'a str,
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// Example { word: "foo" }.to_string(),
|
||||
/// "<div>FOO</div>"
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// Example { word: "FooBar" }.to_string(),
|
||||
/// "<div>FOOBAR</div>"
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn upper(s: impl fmt::Display) -> Result<String, fmt::Error> {
|
||||
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
|
||||
/// /// <div>{{ word|uppercase }}</div>
|
||||
/// /// ```
|
||||
/// #[derive(Template)]
|
||||
/// #[template(ext = "html", in_doc = true)]
|
||||
/// struct Example<'a> {
|
||||
/// word: &'a str,
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// Example { word: "foo" }.to_string(),
|
||||
/// "<div>FOO</div>"
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// Example { word: "FooBar" }.to_string(),
|
||||
/// "<div>FOOBAR</div>"
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn uppercase(s: impl fmt::Display) -> Result<String, fmt::Error> {
|
||||
upper(s)
|
||||
}
|
||||
|
||||
/// Strip leading and trailing whitespace
|
||||
///
|
||||
/// ```
|
||||
/// # #[cfg(feature = "code-in-doc")] {
|
||||
/// # use rinja::Template;
|
||||
/// /// ```jinja
|
||||
/// /// <div>{{ example|trim }}</div>
|
||||
/// /// ```
|
||||
/// #[derive(Template)]
|
||||
/// #[template(ext = "html", in_doc = true)]
|
||||
/// struct Example<'a> {
|
||||
/// example: &'a str,
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// Example { example: " Hello\tworld\t" }.to_string(),
|
||||
/// "<div>Hello\tworld</div>"
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn trim<T: fmt::Display>(s: T) -> Result<String> {
|
||||
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
|
||||
///
|
||||
/// ```
|
||||
@ -465,62 +114,6 @@ impl<W: fmt::Write> fmt::Write for TruncateWriter<W> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Indent lines with `width` spaces
|
||||
///
|
||||
/// ```
|
||||
/// # #[cfg(feature = "code-in-doc")] {
|
||||
/// # use rinja::Template;
|
||||
/// /// ```jinja
|
||||
/// /// <div>{{ example|indent(4) }}</div>
|
||||
/// /// ```
|
||||
/// #[derive(Template)]
|
||||
/// #[template(ext = "html", in_doc = true)]
|
||||
/// struct Example<'a> {
|
||||
/// example: &'a str,
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// Example { example: "hello\nfoo\nbar" }.to_string(),
|
||||
/// "<div>hello\n foo\n bar</div>"
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn indent(s: impl fmt::Display, width: usize) -> Result<String, fmt::Error> {
|
||||
fn indent(args: fmt::Arguments<'_>, width: usize) -> Result<String, fmt::Error> {
|
||||
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
|
||||
///
|
||||
/// ```
|
||||
@ -585,49 +178,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Capitalize a value. The first character will be uppercase, all others lowercase.
|
||||
///
|
||||
/// ```
|
||||
/// # #[cfg(feature = "code-in-doc")] {
|
||||
/// # use rinja::Template;
|
||||
/// /// ```jinja
|
||||
/// /// <div>{{ example|capitalize }}</div>
|
||||
/// /// ```
|
||||
/// #[derive(Template)]
|
||||
/// #[template(ext = "html", in_doc = true)]
|
||||
/// struct Example<'a> {
|
||||
/// example: &'a str,
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// Example { example: "hello" }.to_string(),
|
||||
/// "<div>Hello</div>"
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// Example { example: "hElLO" }.to_string(),
|
||||
/// "<div>Hello</div>"
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn capitalize(s: impl fmt::Display) -> Result<String, fmt::Error> {
|
||||
fn capitalize(s: &str) -> Result<String, fmt::Error> {
|
||||
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
|
||||
///
|
||||
/// ```
|
||||
@ -668,80 +218,6 @@ impl<T: fmt::Display> fmt::Display for Center<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Count the words in that string.
|
||||
///
|
||||
/// ```
|
||||
/// # #[cfg(feature = "code-in-doc")] {
|
||||
/// # use rinja::Template;
|
||||
/// /// ```jinja
|
||||
/// /// <div>{{ example|wordcount }}</div>
|
||||
/// /// ```
|
||||
/// #[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(),
|
||||
/// "<div>5</div>"
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn wordcount(s: impl fmt::Display) -> Result<usize, fmt::Error> {
|
||||
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
|
||||
/// /// <div>{{ example|title }}</div>
|
||||
/// /// ```
|
||||
/// #[derive(Template)]
|
||||
/// #[template(ext = "html", in_doc = true)]
|
||||
/// struct Example<'a> {
|
||||
/// example: &'a str,
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// Example { example: "hello WORLD" }.to_string(),
|
||||
/// "<div>Hello World</div>"
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn title(s: impl fmt::Display) -> Result<String, fmt::Error> {
|
||||
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
|
||||
@ -930,7 +406,7 @@ const _: () = {
|
||||
/// implement `PluralizeCount` for non-zero integer types
|
||||
macro_rules! impl_pluralize_for_non_zero {
|
||||
($($ty:ident)*) => { $(
|
||||
impl PluralizeCount for std::num::$ty {
|
||||
impl PluralizeCount for core::num::$ty {
|
||||
type Error = Infallible;
|
||||
|
||||
#[inline]
|
||||
@ -972,114 +448,13 @@ impl<S: FastWritable, P: FastWritable> FastWritable for Pluralize<S, P> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(test, feature = "alloc"))]
|
||||
mod tests {
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_linebreaks() {
|
||||
assert_eq!(
|
||||
linebreaks("Foo\nBar Baz").unwrap().to_string(),
|
||||
"<p>Foo<br/>Bar Baz</p>"
|
||||
);
|
||||
assert_eq!(
|
||||
linebreaks("Foo\nBar\n\nBaz").unwrap().to_string(),
|
||||
"<p>Foo<br/>Bar</p><p>Baz</p>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_linebreaksbr() {
|
||||
assert_eq!(linebreaksbr("Foo\nBar").unwrap().to_string(), "Foo<br/>Bar");
|
||||
assert_eq!(
|
||||
linebreaksbr("Foo\nBar\n\nBaz").unwrap().to_string(),
|
||||
"Foo<br/>Bar<br/><br/>Baz"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paragraphbreaks() {
|
||||
assert_eq!(
|
||||
paragraphbreaks("Foo\nBar Baz").unwrap().to_string(),
|
||||
"<p>Foo\nBar Baz</p>"
|
||||
);
|
||||
assert_eq!(
|
||||
paragraphbreaks("Foo\nBar\n\nBaz").unwrap().to_string(),
|
||||
"<p>Foo\nBar</p><p>Baz</p>"
|
||||
);
|
||||
assert_eq!(
|
||||
paragraphbreaks("Foo\n\n\n\n\nBar\n\nBaz")
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
"<p>Foo</p><p>\nBar</p><p>Baz</p>"
|
||||
);
|
||||
}
|
||||
|
||||
#[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() {
|
||||
@ -1097,14 +472,14 @@ mod tests {
|
||||
let empty: &[&str] = &[];
|
||||
assert_eq!(join(empty.iter(), ", ").unwrap().to_string(), "");
|
||||
|
||||
let input: Vec<String> = vec!["foo".into(), "bar".into(), "bazz".into()];
|
||||
let input: Vec<String> = alloc::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];
|
||||
let input: Vec<&str> = alloc::vec![&real];
|
||||
assert_eq!(join(input.iter(), ";").unwrap().to_string(), "blah");
|
||||
|
||||
assert_eq!(
|
||||
@ -1115,25 +490,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[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());
|
||||
@ -1148,31 +504,4 @@ mod tests {
|
||||
"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);
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
use std::convert::Infallible;
|
||||
use std::fmt::{self, Formatter, Write};
|
||||
use std::ops::Deref;
|
||||
use std::pin::Pin;
|
||||
use std::{borrow, str};
|
||||
use core::convert::Infallible;
|
||||
use core::fmt::{self, Formatter, Write};
|
||||
use core::ops::Deref;
|
||||
use core::pin::Pin;
|
||||
use core::str;
|
||||
|
||||
/// Marks a string (or other `Display` type) as safe
|
||||
///
|
||||
@ -460,18 +460,19 @@ mark_html_safe! {
|
||||
f32, f64,
|
||||
i8, i16, i32, i64, i128, isize,
|
||||
u8, u16, u32, u64, u128, usize,
|
||||
std::num::NonZeroI8, std::num::NonZeroI16, std::num::NonZeroI32,
|
||||
std::num::NonZeroI64, std::num::NonZeroI128, std::num::NonZeroIsize,
|
||||
std::num::NonZeroU8, std::num::NonZeroU16, std::num::NonZeroU32,
|
||||
std::num::NonZeroU64, std::num::NonZeroU128, std::num::NonZeroUsize,
|
||||
core::num::NonZeroI8, core::num::NonZeroI16, core::num::NonZeroI32,
|
||||
core::num::NonZeroI64, core::num::NonZeroI128, core::num::NonZeroIsize,
|
||||
core::num::NonZeroU8, core::num::NonZeroU16, core::num::NonZeroU32,
|
||||
core::num::NonZeroU64, core::num::NonZeroU128, core::num::NonZeroUsize,
|
||||
}
|
||||
|
||||
impl<T: HtmlSafe> HtmlSafe for std::num::Wrapping<T> {}
|
||||
impl<T: HtmlSafe> HtmlSafe for core::num::Wrapping<T> {}
|
||||
impl<T: fmt::Display> HtmlSafe for HtmlSafeOutput<T> {}
|
||||
|
||||
impl<T> HtmlSafe for borrow::Cow<'_, T>
|
||||
#[cfg(feature = "alloc")]
|
||||
impl<T> HtmlSafe for alloc::borrow::Cow<'_, T>
|
||||
where
|
||||
T: HtmlSafe + borrow::ToOwned + ?Sized,
|
||||
T: HtmlSafe + alloc::borrow::ToOwned + ?Sized,
|
||||
T::Owned: HtmlSafe,
|
||||
{
|
||||
}
|
||||
@ -520,7 +521,8 @@ const _: () = {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: FastWritable + ToOwned> FastWritable for borrow::Cow<'_, T> {
|
||||
#[cfg(feature = "alloc")]
|
||||
impl<T: FastWritable + alloc::borrow::ToOwned> FastWritable for alloc::borrow::Cow<'_, T> {
|
||||
#[inline]
|
||||
fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> crate::Result<()> {
|
||||
T::write_into(self.as_ref(), dest)
|
||||
@ -568,7 +570,8 @@ const _: () = {
|
||||
}
|
||||
}
|
||||
|
||||
impl FastWritable for String {
|
||||
#[cfg(feature = "alloc")]
|
||||
impl FastWritable for alloc::string::String {
|
||||
#[inline]
|
||||
fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> crate::Result<()> {
|
||||
self.as_str().write_into(dest)
|
||||
@ -617,7 +620,10 @@ const _: () = {
|
||||
};
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "alloc")]
|
||||
fn test_escape() {
|
||||
use alloc::string::ToString;
|
||||
|
||||
assert_eq!(escape("", Html).unwrap().to_string(), "");
|
||||
assert_eq!(escape("<&>", Html).unwrap().to_string(), "<&>");
|
||||
assert_eq!(escape("bla&", Html).unwrap().to_string(), "bla&");
|
||||
@ -632,7 +638,10 @@ fn test_escape() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "alloc")]
|
||||
fn test_html_safe_marker() {
|
||||
use alloc::string::ToString;
|
||||
|
||||
struct Script1;
|
||||
struct Script2;
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
use std::convert::Infallible;
|
||||
use std::fmt;
|
||||
use std::mem::MaybeUninit;
|
||||
use std::str::from_utf8_unchecked;
|
||||
use core::convert::Infallible;
|
||||
use core::fmt;
|
||||
use core::mem::MaybeUninit;
|
||||
use core::str::from_utf8_unchecked;
|
||||
|
||||
use super::FastWritable;
|
||||
|
||||
@ -101,8 +101,10 @@ const SI_PREFIXES: &[((u8, f32), f32)] = &[
|
||||
];
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::needless_borrows_for_generic_args)]
|
||||
#[cfg(feature = "alloc")]
|
||||
fn test_filesizeformat() {
|
||||
use alloc::string::ToString;
|
||||
|
||||
assert_eq!(filesizeformat(0.).unwrap().to_string(), "0 B");
|
||||
assert_eq!(filesizeformat(999.).unwrap().to_string(), "999 B");
|
||||
assert_eq!(filesizeformat(1000.).unwrap().to_string(), "1 kB");
|
||||
|
@ -119,7 +119,8 @@ impl AsIndent for str {
|
||||
}
|
||||
}
|
||||
|
||||
impl AsIndent for String {
|
||||
#[cfg(feature = "alloc")]
|
||||
impl AsIndent for alloc::string::String {
|
||||
#[inline]
|
||||
fn as_indent(&self) -> &str {
|
||||
self
|
||||
@ -157,7 +158,8 @@ fn spaces(width: usize) -> &'static str {
|
||||
&SPACES[..width.min(SPACES.len())]
|
||||
}
|
||||
|
||||
impl<T: AsIndent + ToOwned + ?Sized> AsIndent for std::borrow::Cow<'_, T> {
|
||||
#[cfg(feature = "alloc")]
|
||||
impl<T: AsIndent + alloc::borrow::ToOwned + ?Sized> AsIndent for alloc::borrow::Cow<'_, T> {
|
||||
#[inline]
|
||||
fn as_indent(&self) -> &str {
|
||||
T::as_indent(self)
|
||||
@ -265,8 +267,11 @@ fn write<W: fmt::Write + ?Sized>(f: &mut W, bytes: &[u8]) -> fmt::Result {
|
||||
f.write_str(unsafe { str::from_utf8_unchecked(&bytes[last..]) })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(test, feature = "alloc"))]
|
||||
mod tests {
|
||||
use alloc::string::ToString;
|
||||
use alloc::vec;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
|
@ -10,6 +10,8 @@
|
||||
//! The traits [`AutoEscape`] and [`WriteWritable`] are used by [`rinja_derive`]'s generated code
|
||||
//! to work with all compatible types.
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
mod alloc;
|
||||
mod builtin;
|
||||
mod escape;
|
||||
mod humansize;
|
||||
@ -18,10 +20,12 @@ mod json;
|
||||
#[cfg(feature = "urlencode")]
|
||||
mod urlencode;
|
||||
|
||||
pub use self::builtin::{
|
||||
PluralizeCount, capitalize, center, fmt, format, indent, join, linebreaks, linebreaksbr, lower,
|
||||
lowercase, paragraphbreaks, pluralize, title, trim, truncate, upper, uppercase, wordcount,
|
||||
#[cfg(feature = "alloc")]
|
||||
pub use self::alloc::{
|
||||
capitalize, fmt, format, indent, linebreaks, linebreaksbr, lower, lowercase, paragraphbreaks,
|
||||
title, trim, upper, uppercase, wordcount,
|
||||
};
|
||||
pub use self::builtin::{PluralizeCount, center, join, pluralize, truncate};
|
||||
pub use self::escape::{
|
||||
AutoEscape, AutoEscaper, Escaper, FastWritable, Html, HtmlSafe, HtmlSafeOutput, MaybeSafe,
|
||||
Safe, Text, Unsafe, Writable, WriteWritable, e, escape, safe,
|
||||
@ -31,3 +35,6 @@ pub use self::humansize::filesizeformat;
|
||||
pub use self::json::{AsIndent, json, json_pretty};
|
||||
#[cfg(feature = "urlencode")]
|
||||
pub use self::urlencode::{urlencode, urlencode_strict};
|
||||
|
||||
// MAX_LEN is maximum allowed length for filters.
|
||||
const MAX_LEN: usize = 10_000;
|
||||
|
@ -127,7 +127,10 @@ impl<W: fmt::Write> fmt::Write for UrlencodeWriter<W> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "alloc")]
|
||||
fn test_urlencoding() {
|
||||
use alloc::string::ToString;
|
||||
|
||||
// Unreserved (https://tools.ietf.org/html/rfc3986.html#section-2.3)
|
||||
// alpha / digit
|
||||
assert_eq!(urlencode("AZaz09").unwrap().to_string(), "AZaz09");
|
||||
|
@ -1,12 +1,16 @@
|
||||
use std::cell::Cell;
|
||||
use std::fmt;
|
||||
use std::iter::{Enumerate, Peekable};
|
||||
use std::ops::Deref;
|
||||
use std::pin::Pin;
|
||||
|
||||
// The re-exports are used in the generated code for macro hygiene. Even if the paths `::core` or
|
||||
// `::std` are shadowed, the generated code will still be able to access the crates.
|
||||
pub use {core, std};
|
||||
#[cfg(feature = "alloc")]
|
||||
pub extern crate alloc;
|
||||
pub extern crate core;
|
||||
#[cfg(feature = "std")]
|
||||
pub extern crate std;
|
||||
|
||||
use core::cell::Cell;
|
||||
use core::fmt;
|
||||
use core::iter::{Enumerate, Peekable};
|
||||
use core::ops::Deref;
|
||||
use core::pin::Pin;
|
||||
|
||||
use crate::filters::FastWritable;
|
||||
|
||||
@ -177,7 +181,7 @@ impl<T: PrimitiveType + Copy> PrimitiveType for Cell<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PrimitiveType> PrimitiveType for std::num::Wrapping<T> {
|
||||
impl<T: PrimitiveType> PrimitiveType for core::num::Wrapping<T> {
|
||||
type Value = T::Value;
|
||||
|
||||
#[inline]
|
||||
@ -186,7 +190,7 @@ impl<T: PrimitiveType> PrimitiveType for std::num::Wrapping<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PrimitiveType> PrimitiveType for std::num::Saturating<T> {
|
||||
impl<T: PrimitiveType> PrimitiveType for core::num::Saturating<T> {
|
||||
type Value = T::Value;
|
||||
|
||||
#[inline]
|
||||
@ -209,18 +213,18 @@ macro_rules! primitize_nz {
|
||||
}
|
||||
|
||||
primitize_nz! {
|
||||
std::num::NonZeroI8 => i8,
|
||||
std::num::NonZeroI16 => i16,
|
||||
std::num::NonZeroI32 => i32,
|
||||
std::num::NonZeroI64 => i64,
|
||||
std::num::NonZeroI128 => i128,
|
||||
std::num::NonZeroIsize => isize,
|
||||
std::num::NonZeroU8 => u8,
|
||||
std::num::NonZeroU16 => u16,
|
||||
std::num::NonZeroU32 => u32,
|
||||
std::num::NonZeroU64 => u64,
|
||||
std::num::NonZeroU128 => u128,
|
||||
std::num::NonZeroUsize => usize,
|
||||
core::num::NonZeroI8 => i8,
|
||||
core::num::NonZeroI16 => i16,
|
||||
core::num::NonZeroI32 => i32,
|
||||
core::num::NonZeroI64 => i64,
|
||||
core::num::NonZeroI128 => i128,
|
||||
core::num::NonZeroIsize => isize,
|
||||
core::num::NonZeroU8 => u8,
|
||||
core::num::NonZeroU16 => u16,
|
||||
core::num::NonZeroU32 => u32,
|
||||
core::num::NonZeroU64 => u64,
|
||||
core::num::NonZeroU128 => u128,
|
||||
core::num::NonZeroUsize => usize,
|
||||
}
|
||||
|
||||
/// An empty element, so nothing will be written.
|
||||
@ -265,9 +269,10 @@ impl<L: FastWritable, R: FastWritable> FastWritable for Concat<L, R> {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(feature = "alloc")]
|
||||
pub fn map_try<T, E>(result: Result<T, E>) -> Result<T, crate::Error>
|
||||
where
|
||||
E: Into<Box<dyn std::error::Error + Send + Sync>>,
|
||||
E: Into<alloc::boxed::Box<dyn std::error::Error + Send + Sync>>,
|
||||
{
|
||||
result.map_err(crate::Error::custom)
|
||||
}
|
||||
|
@ -1,4 +1,9 @@
|
||||
use std::{fmt, str};
|
||||
// The file is shared across many crates, not all have this feature.
|
||||
// If they don't then the tests won't be compiled in, but that's OK, because they are executed at
|
||||
// least in the crate `rinja`. There's no need to run the test multiple times.
|
||||
#![allow(unexpected_cfgs)]
|
||||
|
||||
use core::{fmt, str};
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) fn write_escaped_str(mut dest: impl fmt::Write, src: &str) -> fmt::Result {
|
||||
@ -116,8 +121,11 @@ const ESCAPED_BUF_INIT: [u8; 8] = *b"&#__;\0\0\0";
|
||||
const ESCAPED_BUF_LEN: usize = b"&#__;".len();
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "alloc")]
|
||||
fn test_simple_html_string_escaping() {
|
||||
let mut buf = String::new();
|
||||
extern crate alloc;
|
||||
|
||||
let mut buf = alloc::string::String::new();
|
||||
write_escaped_str(&mut buf, "<script>").unwrap();
|
||||
assert_eq!(buf, "<script>");
|
||||
|
||||
|
@ -58,6 +58,12 @@
|
||||
#![deny(elided_lifetimes_in_paths)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![deny(missing_docs)]
|
||||
#![no_std]
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
extern crate alloc;
|
||||
#[cfg(feature = "std")]
|
||||
extern crate std;
|
||||
|
||||
mod error;
|
||||
pub mod filters;
|
||||
@ -65,7 +71,11 @@ pub mod filters;
|
||||
pub mod helpers;
|
||||
mod html;
|
||||
|
||||
use std::{fmt, io};
|
||||
#[cfg(feature = "alloc")]
|
||||
use alloc::string::String;
|
||||
use core::fmt;
|
||||
#[cfg(feature = "std")]
|
||||
use std::io;
|
||||
|
||||
pub use rinja_derive::Template;
|
||||
|
||||
@ -86,9 +96,9 @@ pub use crate::helpers::PrimitiveType;
|
||||
/// * [`.render_into()`][Template::render_into] (to render the content into an [`fmt::Write`]
|
||||
/// object, e.g. [`String`]) or
|
||||
/// * [`.write_into()`][Template::write_into] (to render the content into an [`io::Write`] object,
|
||||
/// e.g. [`Vec<u8>`])
|
||||
/// e.g. [`Vec<u8>`][alloc::vec::Vec])
|
||||
///
|
||||
/// over [`.to_string()`][std::string::ToString::to_string] or [`format!()`].
|
||||
/// over [`.to_string()`][std::string::ToString::to_string] or [`format!()`][alloc::format].
|
||||
/// While `.to_string()` and `format!()` give you the same result, they generally perform much worse
|
||||
/// than rinja's own methods, because [`fmt::Write`] uses [dynamic methods calls] instead of
|
||||
/// monomorphised code. On average, expect `.to_string()` to be 100% to 200% slower than
|
||||
@ -97,6 +107,7 @@ pub use crate::helpers::PrimitiveType;
|
||||
/// [dynamic methods calls]: <https://doc.rust-lang.org/stable/std/keyword.dyn.html>
|
||||
pub trait Template: fmt::Display + filters::FastWritable {
|
||||
/// Helper method which allocates a new `String` and renders into it
|
||||
#[cfg(feature = "alloc")]
|
||||
fn render(&self) -> Result<String> {
|
||||
let mut buf = String::new();
|
||||
let _ = buf.try_reserve(Self::SIZE_HINT);
|
||||
@ -108,6 +119,7 @@ pub trait Template: fmt::Display + filters::FastWritable {
|
||||
fn render_into<W: fmt::Write + ?Sized>(&self, writer: &mut W) -> Result<()>;
|
||||
|
||||
/// Renders the template to the given `writer` io buffer
|
||||
#[cfg(feature = "std")]
|
||||
fn write_into<W: io::Write + ?Sized>(&self, writer: &mut W) -> io::Result<()> {
|
||||
struct Wrapped<W: io::Write> {
|
||||
writer: W,
|
||||
@ -143,6 +155,7 @@ pub trait Template: fmt::Display + filters::FastWritable {
|
||||
/// [`render`]: Template::render
|
||||
/// [`render_into`]: Template::render_into
|
||||
/// [`write_into`]: Template::write_into
|
||||
/// [`ToString::to_string`]: alloc::string::ToString::to_string
|
||||
const SIZE_HINT: usize;
|
||||
}
|
||||
|
||||
@ -153,11 +166,13 @@ impl<T: Template + ?Sized> Template for &T {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(feature = "alloc")]
|
||||
fn render(&self) -> Result<String> {
|
||||
<T as Template>::render(self)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(feature = "std")]
|
||||
fn write_into<W: io::Write + ?Sized>(&self, writer: &mut W) -> io::Result<()> {
|
||||
<T as Template>::write_into(self, writer)
|
||||
}
|
||||
@ -170,12 +185,14 @@ impl<T: Template + ?Sized> Template for &T {
|
||||
/// This trades reduced performance (mostly due to writing into `dyn Write`) for object safety.
|
||||
pub trait DynTemplate {
|
||||
/// Helper method which allocates a new `String` and renders into it
|
||||
#[cfg(feature = "alloc")]
|
||||
fn dyn_render(&self) -> Result<String>;
|
||||
|
||||
/// Renders the template to the given `writer` fmt buffer
|
||||
fn dyn_render_into(&self, writer: &mut dyn fmt::Write) -> Result<()>;
|
||||
|
||||
/// Renders the template to the given `writer` io buffer
|
||||
#[cfg(feature = "std")]
|
||||
fn dyn_write_into(&self, writer: &mut dyn io::Write) -> io::Result<()>;
|
||||
|
||||
/// Provides a conservative estimate of the expanded length of the rendered template
|
||||
@ -183,6 +200,7 @@ pub trait DynTemplate {
|
||||
}
|
||||
|
||||
impl<T: Template> DynTemplate for T {
|
||||
#[cfg(feature = "alloc")]
|
||||
fn dyn_render(&self) -> Result<String> {
|
||||
<Self as Template>::render(self)
|
||||
}
|
||||
@ -192,6 +210,7 @@ impl<T: Template> DynTemplate for T {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(feature = "std")]
|
||||
fn dyn_write_into(&self, writer: &mut dyn io::Write) -> io::Result<()> {
|
||||
<Self as Template>::write_into(self, writer)
|
||||
}
|
||||
@ -210,20 +229,36 @@ impl fmt::Display for dyn DynTemplate {
|
||||
/// Implement the trait `$Trait` for a list of reference (wrapper) types to `$T: $Trait + ?Sized`
|
||||
macro_rules! impl_for_ref {
|
||||
(impl $Trait:ident for $T:ident $body:tt) => {
|
||||
crate::impl_for_ref! {
|
||||
impl<$T> $Trait for [
|
||||
&T
|
||||
&mut T
|
||||
Box<T>
|
||||
std::cell::Ref<'_, T>
|
||||
std::cell::RefMut<'_, T>
|
||||
std::rc::Rc<T>
|
||||
std::sync::Arc<T>
|
||||
std::sync::MutexGuard<'_, T>
|
||||
std::sync::RwLockReadGuard<'_, T>
|
||||
std::sync::RwLockWriteGuard<'_, T>
|
||||
] $body
|
||||
}
|
||||
const _: () = {
|
||||
crate::impl_for_ref! {
|
||||
impl<$T> $Trait for [
|
||||
&T
|
||||
&mut T
|
||||
core::cell::Ref<'_, T>
|
||||
core::cell::RefMut<'_, T>
|
||||
] $body
|
||||
}
|
||||
};
|
||||
#[cfg(feature = "alloc")]
|
||||
const _: () = {
|
||||
crate::impl_for_ref! {
|
||||
impl<$T> $Trait for [
|
||||
alloc::boxed::Box<T>
|
||||
alloc::rc::Rc<T>
|
||||
alloc::sync::Arc<T>
|
||||
] $body
|
||||
}
|
||||
};
|
||||
#[cfg(feature = "std")]
|
||||
const _: () = {
|
||||
crate::impl_for_ref! {
|
||||
impl<$T> $Trait for [
|
||||
std::sync::MutexGuard<'_, T>
|
||||
std::sync::RwLockReadGuard<'_, T>
|
||||
std::sync::RwLockWriteGuard<'_, T>
|
||||
] $body
|
||||
}
|
||||
};
|
||||
};
|
||||
(impl<$T:ident> $Trait:ident for [$($ty:ty)*] $body:tt) => {
|
||||
$(impl<$T: $Trait + ?Sized> $Trait for $ty $body)*
|
||||
@ -232,7 +267,7 @@ macro_rules! impl_for_ref {
|
||||
|
||||
pub(crate) use impl_for_ref;
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(test, feature = "alloc"))]
|
||||
mod tests {
|
||||
use std::fmt;
|
||||
|
||||
@ -241,6 +276,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn dyn_template() {
|
||||
use alloc::string::ToString;
|
||||
|
||||
struct Test;
|
||||
|
||||
impl Template for Test {
|
||||
@ -275,10 +312,10 @@ mod tests {
|
||||
|
||||
assert_eq!(test.to_string(), "test");
|
||||
|
||||
assert_eq!(format!("{test}"), "test");
|
||||
assert_eq!(alloc::format!("{test}"), "test");
|
||||
|
||||
let mut vec = Vec::new();
|
||||
let mut vec = alloc::vec![];
|
||||
test.dyn_write_into(&mut vec).unwrap();
|
||||
assert_eq!(vec, vec![b't', b'e', b's', b't']);
|
||||
assert_eq!(vec, alloc::vec![b't', b'e', b's', b't']);
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ license = "MIT OR Apache-2.0"
|
||||
workspace = ".."
|
||||
readme = "README.md"
|
||||
edition = "2021"
|
||||
rust-version = "1.80"
|
||||
rust-version = "1.81"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
@ -39,10 +39,12 @@ similar = "2.6.0"
|
||||
syn = { version = "2.0.3", features = ["full"] }
|
||||
|
||||
[features]
|
||||
alloc = []
|
||||
code-in-doc = ["dep:pulldown-cmark"]
|
||||
config = ["dep:serde", "dep:basic-toml", "parser/config"]
|
||||
urlencode = []
|
||||
serde_json = []
|
||||
std = ["alloc"]
|
||||
|
||||
[lints.rust]
|
||||
# Used in `rinja_derive_standalone` which uses the same source folder, but is not a proc-macro.
|
||||
|
@ -19,7 +19,10 @@ use crate::heritage::{Context, Heritage};
|
||||
use crate::html::write_escaped_str;
|
||||
use crate::input::{Source, TemplateInput};
|
||||
use crate::integration::{Buffer, impl_everything, write_header};
|
||||
use crate::{BUILTIN_FILTERS, CompileError, FileInfo, MsgValidEscapers, fmt_left, fmt_right};
|
||||
use crate::{
|
||||
BUILTIN_FILTERS, BUILTIN_FILTERS_NEED_ALLOC, CompileError, FileInfo, MsgValidEscapers,
|
||||
fmt_left, fmt_right,
|
||||
};
|
||||
|
||||
pub(crate) fn template_to_string(
|
||||
buf: &mut Buffer,
|
||||
@ -1493,6 +1496,13 @@ impl<'a, 'h> Generator<'a, 'h> {
|
||||
buf: &mut Buffer,
|
||||
expr: &WithSpan<'_, Expr<'_>>,
|
||||
) -> Result<DisplayWrap, CompileError> {
|
||||
if !cfg!(feature = "alloc") {
|
||||
return Err(ctx.generate_error(
|
||||
"the `?` operator requires the `alloc` feature to be enabled",
|
||||
expr.span(),
|
||||
));
|
||||
}
|
||||
|
||||
buf.write("rinja::helpers::map_try(");
|
||||
self.visit_expr(ctx, buf, expr)?;
|
||||
buf.write(")?");
|
||||
@ -1541,8 +1551,11 @@ impl<'a, 'h> Generator<'a, 'h> {
|
||||
buf: &mut Buffer,
|
||||
name: &str,
|
||||
args: &[WithSpan<'_, Expr<'_>>],
|
||||
_node: Span<'_>,
|
||||
node: Span<'_>,
|
||||
) -> Result<DisplayWrap, CompileError> {
|
||||
if BUILTIN_FILTERS_NEED_ALLOC.contains(&name) {
|
||||
ensure_filter_has_feature_alloc(ctx, name, node)?;
|
||||
}
|
||||
buf.write(format_args!("filters::{name}("));
|
||||
self._visit_args(ctx, buf, args)?;
|
||||
buf.write(")?");
|
||||
@ -1657,6 +1670,7 @@ impl<'a, 'h> Generator<'a, 'h> {
|
||||
args: &[WithSpan<'_, Expr<'_>>],
|
||||
node: Span<'_>,
|
||||
) -> Result<DisplayWrap, CompileError> {
|
||||
ensure_filter_has_feature_alloc(ctx, name, node)?;
|
||||
if args.len() != 1 {
|
||||
return Err(ctx.generate_error(
|
||||
format_args!("unexpected argument(s) in `{name}` filter"),
|
||||
@ -1816,10 +1830,11 @@ impl<'a, 'h> Generator<'a, 'h> {
|
||||
&mut self,
|
||||
ctx: &Context<'_>,
|
||||
buf: &mut Buffer,
|
||||
_name: &str,
|
||||
name: &str,
|
||||
args: &[WithSpan<'_, Expr<'_>>],
|
||||
node: Span<'_>,
|
||||
) -> Result<DisplayWrap, CompileError> {
|
||||
ensure_filter_has_feature_alloc(ctx, name, node)?;
|
||||
if !args.is_empty() {
|
||||
if let Expr::StrLit(ref fmt) = *args[0] {
|
||||
buf.write("rinja::helpers::std::format!(");
|
||||
@ -1839,10 +1854,11 @@ impl<'a, 'h> Generator<'a, 'h> {
|
||||
&mut self,
|
||||
ctx: &Context<'_>,
|
||||
buf: &mut Buffer,
|
||||
_name: &str,
|
||||
name: &str,
|
||||
args: &[WithSpan<'_, Expr<'_>>],
|
||||
node: Span<'_>,
|
||||
) -> Result<DisplayWrap, CompileError> {
|
||||
ensure_filter_has_feature_alloc(ctx, name, node)?;
|
||||
if let [_, arg2] = args {
|
||||
if let Expr::StrLit(ref fmt) = **arg2 {
|
||||
buf.write("rinja::helpers::std::format!(");
|
||||
@ -2387,6 +2403,20 @@ impl<'a, 'h> Generator<'a, 'h> {
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_filter_has_feature_alloc(
|
||||
ctx: &Context<'_>,
|
||||
name: &str,
|
||||
node: Span<'_>,
|
||||
) -> Result<(), CompileError> {
|
||||
if !cfg!(feature = "alloc") {
|
||||
return Err(ctx.generate_error(
|
||||
format_args!("the `{name}` filter requires the `alloc` feature to be enabled"),
|
||||
node,
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn macro_call_ensure_arg_count(
|
||||
call: &WithSpan<'_, Call<'_>>,
|
||||
def: &Macro<'_>,
|
||||
|
@ -481,3 +481,6 @@ const BUILTIN_FILTERS: &[&str] = &[
|
||||
"uppercase",
|
||||
"wordcount",
|
||||
];
|
||||
|
||||
// Built-in filters that need the `alloc` feature.
|
||||
const BUILTIN_FILTERS_NEED_ALLOC: &[&str] = &["center", "truncate"];
|
||||
|
@ -8,7 +8,7 @@ repository = "https://github.com/rinja-rs/rinja"
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
edition = "2021"
|
||||
rust-version = "1.80"
|
||||
rust-version = "1.81"
|
||||
publish = false
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
@ -51,5 +51,9 @@ config = ["dep:serde", "dep:basic-toml", "parser/config"]
|
||||
urlencode = []
|
||||
serde_json = []
|
||||
|
||||
[lints.rust]
|
||||
# Used in `rinja_derive` which uses the same source folder, but is a proc-macro.
|
||||
unexpected_cfgs = { level = "allow", check-cfg = ['cfg(feature, values("alloc"))'] }
|
||||
|
||||
[workspace]
|
||||
members = ["."]
|
||||
|
@ -11,7 +11,7 @@ license = "MIT OR Apache-2.0"
|
||||
workspace = ".."
|
||||
readme = "README.md"
|
||||
edition = "2021"
|
||||
rust-version = "1.80"
|
||||
rust-version = "1.81"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
@ -4,7 +4,7 @@ version = "0.3.5"
|
||||
authors = ["rinja-rs developers"]
|
||||
workspace = ".."
|
||||
edition = "2021"
|
||||
rust-version = "1.80"
|
||||
rust-version = "1.81"
|
||||
publish = false
|
||||
|
||||
[features]
|
||||
|
Loading…
x
Reference in New Issue
Block a user