Make #![no_std] compatible

This commit is contained in:
René Kijewski 2024-12-13 23:08:02 +01:00
parent f00e5e2091
commit bedc31797c
19 changed files with 269 additions and 1317 deletions

View File

@ -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:

View File

@ -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"

View File

@ -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]&lt;dyn [StdError] + [Send] + [Sync]&gt;</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
}
}

View File

@ -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 &#60;b&#62;ONE&#60;/b&#62;!",
/// );
/// 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);

View File

@ -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);
}
}

View File

@ -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(), "&#60;&#38;&#62;");
assert_eq!(escape("bla&", Html).unwrap().to_string(), "bla&#38;");
@ -632,7 +638,10 @@ fn test_escape() {
}
#[test]
#[cfg(feature = "alloc")]
fn test_html_safe_marker() {
use alloc::string::ToString;
struct Script1;
struct Script2;

View File

@ -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");

View File

@ -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]

View File

@ -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;

View File

@ -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");

View File

@ -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)
}

View File

@ -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, "&#60;script&#62;");

View File

@ -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']);
}
}

View File

@ -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.

View File

@ -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<'_>,

View File

@ -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"];

View File

@ -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 = ["."]

View File

@ -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

View File

@ -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]