Merge pull request #397 from Kijewski/pr-fast-writable-values

Pass variables to sub-templates more reliably even if indirectly
This commit is contained in:
Guillaume Gomez 2025-04-17 11:41:54 +02:00 committed by GitHub
commit 2c1e86e410
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 772 additions and 203 deletions

View File

@ -1,8 +1,11 @@
use alloc::format;
use alloc::string::String;
use core::cell::Cell;
use core::convert::Infallible;
use core::fmt::{self, Write};
use super::MAX_LEN;
use super::escape::HtmlSafeOutput;
use super::{FastWritable, MAX_LEN};
use crate::Result;
/// Return an ephemeral `&str` for `$src: impl fmt::Display`
@ -118,14 +121,36 @@ pub fn format() {}
/// # }
/// ```
#[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/>");
alloc::format!("<p>{linebroken}</p>")
}
pub fn linebreaks<S: fmt::Display>(source: S) -> Result<HtmlSafeOutput<Linebreaks<S>>, Infallible> {
Ok(HtmlSafeOutput(Linebreaks(source)))
}
let mut buffer;
Ok(HtmlSafeOutput(linebreaks(try_to_str!(s => buffer))))
pub struct Linebreaks<S>(S);
impl<S: fmt::Display> fmt::Display for Linebreaks<S> {
#[inline]
fn fmt(&self, dest: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut buffer;
flush_linebreaks(dest, try_to_str!(self.0 => buffer))
}
}
impl<S: FastWritable> FastWritable for Linebreaks<S> {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(
&self,
dest: &mut W,
values: &dyn crate::Values,
) -> crate::Result<()> {
let mut buffer = String::new();
self.0.write_into(&mut buffer, values)?;
Ok(flush_linebreaks(dest, &buffer)?)
}
}
fn flush_linebreaks(dest: &mut (impl fmt::Write + ?Sized), s: &str) -> fmt::Result {
let linebroken = s.replace("\n\n", "</p><p>").replace('\n', "<br/>");
write!(dest, "<p>{linebroken}</p>")
}
/// Converts all newlines in a piece of plain text to HTML line breaks
@ -149,13 +174,37 @@ pub fn linebreaks(s: impl fmt::Display) -> Result<HtmlSafeOutput<String>, fmt::E
/// # }
/// ```
#[inline]
pub fn linebreaksbr(s: impl fmt::Display) -> Result<HtmlSafeOutput<String>, fmt::Error> {
fn linebreaksbr(s: &str) -> String {
s.replace('\n', "<br/>")
}
pub fn linebreaksbr<S: fmt::Display>(
source: S,
) -> Result<HtmlSafeOutput<Linebreaksbr<S>>, Infallible> {
Ok(HtmlSafeOutput(Linebreaksbr(source)))
}
let mut buffer;
Ok(HtmlSafeOutput(linebreaksbr(try_to_str!(s => buffer))))
pub struct Linebreaksbr<S>(S);
impl<S: fmt::Display> fmt::Display for Linebreaksbr<S> {
#[inline]
fn fmt(&self, dest: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut buffer;
flush_linebreaksbr(dest, try_to_str!(self.0 => buffer))
}
}
impl<S: FastWritable> FastWritable for Linebreaksbr<S> {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(
&self,
dest: &mut W,
values: &dyn crate::Values,
) -> crate::Result<()> {
let mut buffer = String::new();
self.0.write_into(&mut buffer, values)?;
Ok(flush_linebreaksbr(dest, &buffer)?)
}
}
fn flush_linebreaksbr(dest: &mut (impl fmt::Write + ?Sized), s: &str) -> fmt::Result {
dest.write_str(&s.replace('\n', "<br/>"))
}
/// Replaces only paragraph breaks in plain text with appropriate HTML
@ -183,14 +232,38 @@ pub fn linebreaksbr(s: impl fmt::Display) -> Result<HtmlSafeOutput<String>, fmt:
/// # }
/// ```
#[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>", "");
alloc::format!("<p>{linebroken}</p>")
}
pub fn paragraphbreaks<S: fmt::Display>(
source: S,
) -> Result<HtmlSafeOutput<Paragraphbreaks<S>>, Infallible> {
Ok(HtmlSafeOutput(Paragraphbreaks(source)))
}
let mut buffer;
Ok(HtmlSafeOutput(paragraphbreaks(try_to_str!(s => buffer))))
pub struct Paragraphbreaks<S>(S);
impl<S: fmt::Display> fmt::Display for Paragraphbreaks<S> {
#[inline]
fn fmt(&self, dest: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut buffer;
flush_paragraphbreaks(dest, try_to_str!(self.0 => buffer))
}
}
impl<S: FastWritable> FastWritable for Paragraphbreaks<S> {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(
&self,
dest: &mut W,
values: &dyn crate::Values,
) -> crate::Result<()> {
let mut buffer = String::new();
self.0.write_into(&mut buffer, values)?;
Ok(flush_paragraphbreaks(dest, &buffer)?)
}
}
fn flush_paragraphbreaks(dest: &mut (impl fmt::Write + ?Sized), s: &str) -> fmt::Result {
let linebroken = s.replace("\n\n", "</p><p>").replace("<p></p>", "");
write!(dest, "<p>{linebroken}</p>")
}
/// Converts to lowercase
@ -219,9 +292,35 @@ pub fn paragraphbreaks(s: impl fmt::Display) -> Result<HtmlSafeOutput<String>, f
/// # }
/// ```
#[inline]
pub fn lower(s: impl fmt::Display) -> Result<String, fmt::Error> {
let mut buffer;
Ok(try_to_str!(s => buffer).to_lowercase())
pub fn lower<S: fmt::Display>(source: S) -> Result<Lower<S>, Infallible> {
Ok(Lower(source))
}
pub struct Lower<S>(S);
impl<S: fmt::Display> fmt::Display for Lower<S> {
#[inline]
fn fmt(&self, dest: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut buffer;
flush_lower(dest, try_to_str!(self.0 => buffer))
}
}
impl<S: FastWritable> FastWritable for Lower<S> {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(
&self,
dest: &mut W,
values: &dyn crate::Values,
) -> crate::Result<()> {
let mut buffer = String::new();
self.0.write_into(&mut buffer, values)?;
Ok(flush_lower(dest, &buffer)?)
}
}
fn flush_lower(dest: &mut (impl fmt::Write + ?Sized), s: &str) -> fmt::Result {
dest.write_str(&s.to_lowercase())
}
/// Converts to lowercase, alias for the `|lower` filter
@ -250,8 +349,8 @@ pub fn lower(s: impl fmt::Display) -> Result<String, fmt::Error> {
/// # }
/// ```
#[inline]
pub fn lowercase(s: impl fmt::Display) -> Result<String, fmt::Error> {
lower(s)
pub fn lowercase<S: fmt::Display>(source: S) -> Result<Lower<S>, Infallible> {
lower(source)
}
/// Converts to uppercase
@ -280,9 +379,35 @@ pub fn lowercase(s: impl fmt::Display) -> Result<String, fmt::Error> {
/// # }
/// ```
#[inline]
pub fn upper(s: impl fmt::Display) -> Result<String, fmt::Error> {
let mut buffer;
Ok(try_to_str!(s => buffer).to_uppercase())
pub fn upper<S: fmt::Display>(source: S) -> Result<Upper<S>, Infallible> {
Ok(Upper(source))
}
pub struct Upper<S>(S);
impl<S: fmt::Display> fmt::Display for Upper<S> {
#[inline]
fn fmt(&self, dest: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut buffer;
flush_upper(dest, try_to_str!(self.0 => buffer))
}
}
impl<S: FastWritable> FastWritable for Upper<S> {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(
&self,
dest: &mut W,
values: &dyn crate::Values,
) -> crate::Result<()> {
let mut buffer = String::new();
self.0.write_into(&mut buffer, values)?;
Ok(flush_upper(dest, &buffer)?)
}
}
fn flush_upper(dest: &mut (impl fmt::Write + ?Sized), s: &str) -> fmt::Result {
dest.write_str(&s.to_uppercase())
}
/// Converts to uppercase, alias for the `|upper` filter
@ -311,8 +436,8 @@ pub fn upper(s: impl fmt::Display) -> Result<String, fmt::Error> {
/// # }
/// ```
#[inline]
pub fn uppercase(s: impl fmt::Display) -> Result<String, fmt::Error> {
upper(s)
pub fn uppercase<S: fmt::Display>(source: S) -> Result<Upper<S>, Infallible> {
upper(source)
}
/// Strip leading and trailing whitespace
@ -335,24 +460,48 @@ 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);
#[inline]
pub fn trim<S: fmt::Display>(source: S) -> Result<Trim<S>, Infallible> {
Ok(Trim(source))
}
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),
}
pub struct Trim<S>(S);
impl<S: fmt::Display> fmt::Display for Trim<S> {
#[inline]
fn fmt(&self, dest: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut collector = TrimCollector(String::new());
write!(collector, "{}", self.0)?;
flush_trim(dest, collector)
}
}
impl<S: FastWritable> FastWritable for Trim<S> {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(
&self,
dest: &mut W,
values: &dyn crate::Values,
) -> crate::Result<()> {
let mut collector = TrimCollector(String::new());
self.0.write_into(&mut collector, values)?;
Ok(flush_trim(dest, collector)?)
}
}
struct TrimCollector(String);
impl fmt::Write for TrimCollector {
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)
fn flush_trim(dest: &mut (impl fmt::Write + ?Sized), collector: TrimCollector) -> fmt::Result {
dest.write_str(collector.0.trim_end())
}
/// Indent lines with `width` spaces
@ -376,39 +525,61 @@ pub fn trim<T: fmt::Display>(s: T) -> Result<String> {
/// # }
/// ```
#[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.into());
} else {
s
}
pub fn indent<S: fmt::Display>(source: S, width: usize) -> Result<Indent<S>, Infallible> {
Ok(Indent { source, width })
}
pub struct Indent<S> {
source: S,
width: usize,
}
impl<S: fmt::Display> fmt::Display for Indent<S> {
fn fmt(&self, dest: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self { ref source, width } = *self;
if width >= MAX_LEN || width == 0 {
write!(dest, "{source}")
} 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(' ');
}
}
let mut buffer;
flush_indent(dest, width, try_to_str!(source => buffer))
}
Ok(indented)
}
indent(format_args!("{s}"), width)
}
impl<S: FastWritable> FastWritable for Indent<S> {
fn write_into<W: fmt::Write + ?Sized>(
&self,
dest: &mut W,
values: &dyn crate::Values,
) -> crate::Result<()> {
let Self { ref source, width } = *self;
if width >= MAX_LEN || width == 0 {
source.write_into(dest, values)
} else {
let mut buffer = String::new();
source.write_into(&mut buffer, values)?;
Ok(flush_indent(dest, width, &buffer)?)
}
}
}
fn flush_indent(dest: &mut (impl fmt::Write + ?Sized), width: usize, s: &str) -> fmt::Result {
if s.len() >= MAX_LEN {
return dest.write_str(s);
}
let mut prefix = String::new();
for (idx, line) in s.split_inclusive('\n').enumerate() {
if idx > 0 {
// only allocate prefix if needed, i.e. not for single-line outputs
if prefix.is_empty() {
prefix = format!("{: >1$}", "", width);
}
dest.write_str(&prefix)?;
}
dest.write_str(line)?;
}
Ok(())
}
/// Capitalize a value. The first character will be uppercase, all others lowercase.
@ -437,21 +608,45 @@ pub fn indent(s: impl fmt::Display, width: usize) -> Result<String, fmt::Error>
/// # }
/// ```
#[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())
}
}
pub fn capitalize<S: fmt::Display>(source: S) -> Result<Capitalize<S>, Infallible> {
Ok(Capitalize(source))
}
let mut buffer;
capitalize(try_to_str!(s => buffer))
pub struct Capitalize<S>(S);
impl<S: fmt::Display> fmt::Display for Capitalize<S> {
#[inline]
fn fmt(&self, dest: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut buffer;
flush_capitalize(dest, try_to_str!(self.0 => buffer))
}
}
impl<S: FastWritable> FastWritable for Capitalize<S> {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(
&self,
dest: &mut W,
values: &dyn crate::Values,
) -> crate::Result<()> {
let mut buffer = String::new();
self.0.write_into(&mut buffer, values)?;
Ok(flush_capitalize(dest, &buffer)?)
}
}
fn flush_capitalize(dest: &mut (impl fmt::Write + ?Sized), s: &str) -> fmt::Result {
let mut chars = s.chars();
if let Some(c) = chars.next() {
write!(
dest,
"{}{}",
c.to_uppercase(),
chars.as_str().to_lowercase()
)
} else {
Ok(())
}
}
/// Count the words in that string.
@ -474,9 +669,55 @@ pub fn capitalize(s: impl fmt::Display) -> Result<String, fmt::Error> {
/// );
/// # }
/// ```
pub fn wordcount(s: impl fmt::Display) -> Result<usize, fmt::Error> {
let mut buffer;
Ok(try_to_str!(s => buffer).split_whitespace().count())
#[inline]
pub fn wordcount<S>(source: S) -> Wordcount<S> {
Wordcount(Cell::new(Some(WordcountInner {
source,
buffer: String::new(),
})))
}
pub struct Wordcount<S>(Cell<Option<WordcountInner<S>>>);
struct WordcountInner<S> {
source: S,
buffer: String,
}
impl<S: fmt::Display> fmt::Display for Wordcount<S> {
#[inline]
fn fmt(&self, _: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(mut inner) = self.0.take() {
write!(inner.buffer, "{}", inner.source)?;
self.0.set(Some(inner));
}
Ok(())
}
}
impl<S: FastWritable> FastWritable for Wordcount<S> {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(
&self,
_: &mut W,
values: &dyn crate::Values,
) -> crate::Result<()> {
if let Some(mut inner) = self.0.take() {
inner.source.write_into(&mut inner.buffer, values)?;
self.0.set(Some(inner));
}
Ok(())
}
}
impl<S> Wordcount<S> {
pub fn into_count(self) -> usize {
if let Some(inner) = self.0.into_inner() {
inner.buffer.split_whitespace().count()
} else {
0
}
}
}
/// Return a title cased version of the value. Words will start with uppercase letters, all
@ -500,32 +741,39 @@ pub fn wordcount(s: impl fmt::Display) -> Result<usize, fmt::Error> {
/// );
/// # }
/// ```
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;
#[inline]
pub fn title<S: fmt::Display>(source: S) -> Result<Title<S>, Infallible> {
Ok(Title(source))
}
// 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()),
}
}
pub struct Title<S>(S);
impl<S: fmt::Display> fmt::Display for Title<S> {
#[inline]
fn fmt(&self, dest: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut buffer;
flush_title(dest, try_to_str!(self.0 => buffer))
}
Ok(output)
}
impl<S: FastWritable> FastWritable for Title<S> {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(
&self,
dest: &mut W,
values: &dyn crate::Values,
) -> crate::Result<()> {
let mut buffer = String::new();
self.0.write_into(&mut buffer, values)?;
Ok(flush_title(dest, &buffer)?)
}
}
fn flush_title(dest: &mut (impl fmt::Write + ?Sized), s: &str) -> fmt::Result {
for word in s.split_inclusive(char::is_whitespace) {
flush_capitalize(dest, word)?;
}
Ok(())
}
#[cfg(test)]
@ -533,6 +781,7 @@ mod tests {
use alloc::string::ToString;
use super::*;
use crate::NO_VALUES;
#[test]
fn test_linebreaks() {
@ -630,23 +879,40 @@ mod tests {
#[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);
for &(word, count) in &[
("", 0),
(" \n\t", 0),
("foo", 1),
("foo bar", 2),
("foo bar", 2),
] {
let w = wordcount(word);
let _ = w.to_string();
assert_eq!(w.into_count(), count, "fmt: {word:?}");
let w = wordcount(word);
w.write_into(&mut String::new(), NO_VALUES).unwrap();
assert_eq!(w.into_count(), count, "FastWritable: {word:?}");
}
}
#[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");
assert_eq!(&title("").unwrap().to_string(), "");
assert_eq!(&title(" \n\t").unwrap().to_string(), " \n\t");
assert_eq!(&title("foo").unwrap().to_string(), "Foo");
assert_eq!(&title(" foo").unwrap().to_string(), " Foo");
assert_eq!(&title("foo bar").unwrap().to_string(), "Foo Bar");
assert_eq!(&title("foo bar ").unwrap().to_string(), "Foo Bar ");
assert_eq!(&title("fOO").unwrap().to_string(), "Foo");
assert_eq!(&title("fOo BaR").unwrap().to_string(), "Foo Bar");
assert_eq!(&title("foo\r\nbar").unwrap().to_string(), "Foo\r\nBar");
assert_eq!(
&title("Fo\x0boo\x0coO\u{2002}OO\u{3000}baR")
.unwrap()
.to_string(),
"Fo\x0bOo\x0cOo\u{2002}Oo\u{3000}Bar"
);
}
#[test]

View File

@ -6,7 +6,7 @@ use core::pin::Pin;
use super::MAX_LEN;
use super::escape::FastWritable;
use crate::{Error, Result};
use crate::{Error, Result, Values};
/// Limit string length, appends '...' if truncated
///
@ -50,9 +50,13 @@ impl<S: fmt::Display> fmt::Display for TruncateFilter<S> {
impl<S: FastWritable> FastWritable for TruncateFilter<S> {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> crate::Result<()> {
fn write_into<W: fmt::Write + ?Sized>(
&self,
dest: &mut W,
values: &dyn Values,
) -> crate::Result<()> {
self.source
.write_into(&mut TruncateWriter::new(dest, self.remaining))
.write_into(&mut TruncateWriter::new(dest, self.remaining), values)
}
}
@ -440,10 +444,14 @@ impl<S: fmt::Display, P: fmt::Display> fmt::Display for Pluralize<S, P> {
impl<S: FastWritable, P: FastWritable> FastWritable for Pluralize<S, P> {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> crate::Result<()> {
fn write_into<W: fmt::Write + ?Sized>(
&self,
dest: &mut W,
values: &dyn Values,
) -> crate::Result<()> {
match self {
Pluralize::Singular(value) => value.write_into(dest),
Pluralize::Plural(value) => value.write_into(dest),
Pluralize::Singular(value) => value.write_into(dest, values),
Pluralize::Plural(value) => value.write_into(dest, values),
}
}
}

View File

@ -82,8 +82,12 @@ impl<T: fmt::Display, E: Escaper> fmt::Display for EscapeDisplay<T, E> {
impl<T: FastWritable, E: Escaper> FastWritable for EscapeDisplay<T, E> {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> crate::Result<()> {
self.0.write_into(&mut EscapeWriter(dest, self.1))
fn write_into<W: fmt::Write + ?Sized>(
&self,
dest: &mut W,
values: &dyn Values,
) -> crate::Result<()> {
self.0.write_into(&mut EscapeWriter(dest, self.1), values)
}
}
@ -250,10 +254,14 @@ impl<'a, T: HtmlSafe + ?Sized> AutoEscape for &AutoEscaper<'a, T, Html> {
///
/// ```rust
/// mod filters {
/// use askama::{filters::MaybeSafe, Result};
/// use askama::{filters::MaybeSafe, Result, Values};
///
/// // Do not actually use this filter! It's an intentionally bad example.
/// pub fn backdoor<T: std::fmt::Display>(s: T, enable: &bool) -> Result<MaybeSafe<T>> {
/// pub fn backdoor<T: std::fmt::Display>(
/// s: T,
/// _: &dyn Values,
/// enable: &bool,
/// ) -> Result<MaybeSafe<T>> {
/// Ok(match *enable {
/// true => MaybeSafe::Safe(s),
/// false => MaybeSafe::NeedsEscaping(s),
@ -303,12 +311,16 @@ const _: () = {
// This is the fallback. The filter is not the last element of the filter chain.
impl<T: FastWritable> FastWritable for MaybeSafe<T> {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> crate::Result<()> {
fn write_into<W: fmt::Write + ?Sized>(
&self,
dest: &mut W,
values: &dyn Values,
) -> crate::Result<()> {
let inner = match self {
MaybeSafe::Safe(inner) => inner,
MaybeSafe::NeedsEscaping(inner) => inner,
};
inner.write_into(dest)
inner.write_into(dest, values)
}
}
@ -338,10 +350,14 @@ const _: () = {
}
impl<T: FastWritable + ?Sized, E: Escaper> FastWritable for Wrapped<'_, T, E> {
fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> crate::Result<()> {
fn write_into<W: fmt::Write + ?Sized>(
&self,
dest: &mut W,
values: &dyn Values,
) -> crate::Result<()> {
match *self {
Wrapped::Safe(t) => t.write_into(dest),
Wrapped::NeedsEscaping(t, e) => EscapeDisplay(t, e).write_into(dest),
Wrapped::Safe(t) => t.write_into(dest, values),
Wrapped::NeedsEscaping(t, e) => EscapeDisplay(t, e).write_into(dest, values),
}
}
}
@ -368,10 +384,10 @@ const _: () = {
///
/// ```rust
/// mod filters {
/// use askama::{filters::Safe, Result};
/// use askama::{filters::Safe, Result, Values};
///
/// // Do not actually use this filter! It's an intentionally bad example.
/// pub fn strip_except_apos(s: impl ToString) -> Result<Safe<String>> {
/// pub fn strip_except_apos(s: impl ToString, _: &dyn Values) -> Result<Safe<String>> {
/// Ok(Safe(s
/// .to_string()
/// .chars()
@ -409,8 +425,12 @@ const _: () = {
// This is the fallback. The filter is not the last element of the filter chain.
impl<T: FastWritable> FastWritable for Safe<T> {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> crate::Result<()> {
self.0.write_into(dest)
fn write_into<W: fmt::Write + ?Sized>(
&self,
dest: &mut W,
values: &dyn Values,
) -> crate::Result<()> {
self.0.write_into(dest, values)
}
}
@ -503,15 +523,23 @@ pub trait WriteWritable {
/// Types implementing this trait can be written without needing to employ an [`fmt::Formatter`].
pub trait FastWritable {
/// Used internally by askama to speed up writing some types.
fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> crate::Result<()>;
fn write_into<W: fmt::Write + ?Sized>(
&self,
dest: &mut W,
values: &dyn Values,
) -> crate::Result<()>;
}
const _: () = {
crate::impl_for_ref! {
impl FastWritable for T {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> crate::Result<()> {
<T>::write_into(self, dest)
fn write_into<W: fmt::Write + ?Sized>(
&self,
dest: &mut W,
values: &dyn Values,
) -> crate::Result<()> {
<T>::write_into(self, dest, values)
}
}
}
@ -522,16 +550,24 @@ const _: () = {
<T as Deref>::Target: FastWritable,
{
#[inline]
fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> crate::Result<()> {
self.as_ref().get_ref().write_into(dest)
fn write_into<W: fmt::Write + ?Sized>(
&self,
dest: &mut W,
values: &dyn Values,
) -> crate::Result<()> {
self.as_ref().get_ref().write_into(dest, values)
}
}
#[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)
fn write_into<W: fmt::Write + ?Sized>(
&self,
dest: &mut W,
values: &dyn Values,
) -> crate::Result<()> {
T::write_into(self.as_ref(), dest, values)
}
}
@ -540,8 +576,12 @@ const _: () = {
($($ty:ty)*) => { $(
impl FastWritable for $ty {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> crate::Result<()> {
itoa::Buffer::new().format(*self).write_into(dest)
fn write_into<W: fmt::Write + ?Sized>(
&self,
dest: &mut W,
values: &dyn Values,
) -> crate::Result<()> {
itoa::Buffer::new().format(*self).write_into(dest, values)
}
}
)* };
@ -557,8 +597,12 @@ const _: () = {
($($id:ident)*) => { $(
impl FastWritable for core::num::$id {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> crate::Result<()> {
self.get().write_into(dest)
fn write_into<W: fmt::Write + ?Sized>(
&self,
dest: &mut W,
values: &dyn Values,
) -> crate::Result<()> {
self.get().write_into(dest, values)
}
}
)* };
@ -571,7 +615,11 @@ const _: () = {
impl FastWritable for str {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> crate::Result<()> {
fn write_into<W: fmt::Write + ?Sized>(
&self,
dest: &mut W,
_: &dyn Values,
) -> crate::Result<()> {
Ok(dest.write_str(self)?)
}
}
@ -579,14 +627,22 @@ const _: () = {
#[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)
fn write_into<W: fmt::Write + ?Sized>(
&self,
dest: &mut W,
values: &dyn Values,
) -> crate::Result<()> {
self.as_str().write_into(dest, values)
}
}
impl FastWritable for bool {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> crate::Result<()> {
fn write_into<W: fmt::Write + ?Sized>(
&self,
dest: &mut W,
_: &dyn Values,
) -> crate::Result<()> {
Ok(dest.write_str(match self {
true => "true",
false => "false",
@ -596,13 +652,21 @@ const _: () = {
impl FastWritable for char {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> crate::Result<()> {
fn write_into<W: fmt::Write + ?Sized>(
&self,
dest: &mut W,
_: &dyn Values,
) -> crate::Result<()> {
Ok(dest.write_char(*self)?)
}
}
impl FastWritable for fmt::Arguments<'_> {
fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> crate::Result<()> {
fn write_into<W: fmt::Write + ?Sized>(
&self,
dest: &mut W,
_: &dyn Values,
) -> crate::Result<()> {
Ok(match self.as_str() {
Some(s) => dest.write_str(s),
None => dest.write_fmt(*self),
@ -626,9 +690,9 @@ const _: () = {
fn askama_write<W: fmt::Write + ?Sized>(
&self,
dest: &mut W,
_: &dyn Values,
values: &dyn Values,
) -> crate::Result<()> {
self.0.write_into(dest)
self.0.write_into(dest, values)
}
}

View File

@ -4,6 +4,7 @@ use core::mem::MaybeUninit;
use super::FastWritable;
use crate::ascii_str::{AsciiChar, AsciiStr};
use crate::{NO_VALUES, Values};
/// Returns adequate string representation (in KB, ..) of number of bytes
///
@ -33,14 +34,18 @@ pub struct FilesizeFormatFilter(f32);
impl fmt::Display for FilesizeFormatFilter {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Ok(self.write_into(f)?)
Ok(self.write_into(f, NO_VALUES)?)
}
}
impl FastWritable for FilesizeFormatFilter {
fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> crate::Result<()> {
fn write_into<W: fmt::Write + ?Sized>(
&self,
dest: &mut W,
values: &dyn Values,
) -> crate::Result<()> {
if self.0 < 1e3 {
(self.0 as u32).write_into(dest)?;
(self.0 as u32).write_into(dest, values)?;
Ok(dest.write_str(" B")?)
} else if let Some((prefix, factor)) = SI_PREFIXES
.iter()
@ -49,8 +54,8 @@ impl FastWritable for FilesizeFormatFilter {
{
// u32 is big enough to hold the number 999_999
let scaled = (self.0 * factor) as u32;
(scaled / 100).write_into(dest)?;
format_frac(&mut MaybeUninit::uninit(), prefix, scaled).write_into(dest)
(scaled / 100).write_into(dest, values)?;
format_frac(&mut MaybeUninit::uninit(), prefix, scaled).write_into(dest, values)
} else {
too_big(self.0, dest)
}

View File

@ -8,6 +8,7 @@ use serde_json::ser::{CompactFormatter, PrettyFormatter, Serializer};
use super::FastWritable;
use crate::ascii_str::{AsciiChar, AsciiStr};
use crate::{NO_VALUES, Values};
/// Serialize to JSON (requires `json` feature)
///
@ -188,7 +189,8 @@ where
}
impl<S: Serialize> FastWritable for ToJson<S> {
fn write_into<W: fmt::Write + ?Sized>(&self, f: &mut W) -> crate::Result<()> {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(&self, f: &mut W, _: &dyn Values) -> crate::Result<()> {
serialize(f, &self.value, CompactFormatter)
}
}
@ -196,12 +198,13 @@ impl<S: Serialize> FastWritable for ToJson<S> {
impl<S: Serialize> fmt::Display for ToJson<S> {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Ok(self.write_into(f)?)
Ok(self.write_into(f, NO_VALUES)?)
}
}
impl<S: Serialize, I: AsIndent> FastWritable for ToJsonPretty<S, I> {
fn write_into<W: fmt::Write + ?Sized>(&self, f: &mut W) -> crate::Result<()> {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(&self, f: &mut W, _: &dyn Values) -> crate::Result<()> {
serialize(
f,
&self.value,
@ -213,11 +216,10 @@ impl<S: Serialize, I: AsIndent> FastWritable for ToJsonPretty<S, I> {
impl<S: Serialize, I: AsIndent> fmt::Display for ToJsonPretty<S, I> {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Ok(self.write_into(f)?)
Ok(self.write_into(f, NO_VALUES)?)
}
}
#[inline]
fn serialize<S, W, F>(dest: &mut W, value: &S, formatter: F) -> Result<(), crate::Error>
where
S: Serialize + ?Sized,

View File

@ -4,6 +4,7 @@ use std::fmt::Write;
use percent_encoding::{AsciiSet, NON_ALPHANUMERIC, utf8_percent_encode};
use crate::Values;
use crate::filters::{FastWritable, HtmlSafeOutput};
// Urlencode char encoding set. Only the characters in the unreserved set don't
@ -110,8 +111,12 @@ impl<T: fmt::Display> fmt::Display for UrlencodeFilter<T> {
impl<T: FastWritable> FastWritable for UrlencodeFilter<T> {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(&self, f: &mut W) -> crate::Result<()> {
self.0.write_into(&mut UrlencodeWriter(f, self.1))
fn write_into<W: fmt::Write + ?Sized>(
&self,
f: &mut W,
values: &dyn Values,
) -> crate::Result<()> {
self.0.write_into(&mut UrlencodeWriter(f, self.1), values)
}
}

View File

@ -12,6 +12,7 @@ use core::iter::{Enumerate, Peekable};
use core::ops::Deref;
use core::pin::Pin;
use crate::Values;
pub use crate::error::{ErrorMarker, ResultConverter};
use crate::filters::FastWritable;
pub use crate::values::get_value;
@ -245,7 +246,19 @@ impl fmt::Display for Empty {
impl FastWritable for Empty {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(&self, _: &mut W) -> crate::Result<()> {
fn write_into<W: fmt::Write + ?Sized>(&self, _: &mut W, _: &dyn Values) -> crate::Result<()> {
Ok(())
}
}
impl fmt::Write for Empty {
#[inline]
fn write_str(&mut self, _: &str) -> fmt::Result {
Ok(())
}
#[inline]
fn write_char(&mut self, _: char) -> fmt::Result {
Ok(())
}
}
@ -267,9 +280,13 @@ impl<L: fmt::Display, R: fmt::Display> fmt::Display for Concat<L, R> {
impl<L: FastWritable, R: FastWritable> FastWritable for Concat<L, R> {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> crate::Result<()> {
self.0.write_into(dest)?;
self.1.write_into(dest)
fn write_into<W: fmt::Write + ?Sized>(
&self,
dest: &mut W,
values: &dyn Values,
) -> crate::Result<()> {
self.0.write_into(dest, values)?;
self.1.write_into(dest, values)
}
}

View File

@ -406,8 +406,12 @@ mod tests {
impl filters::FastWritable for Test {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(&self, f: &mut W) -> crate::Result<()> {
self.render_into(f)
fn write_into<W: fmt::Write + ?Sized>(
&self,
f: &mut W,
values: &dyn Values,
) -> crate::Result<()> {
self.render_into_with_values(f, values)
}
}

View File

@ -310,11 +310,15 @@ impl<'a, 'h> Generator<'a, 'h> {
impl #wrapper_impl_generics askama::filters::FastWritable
for #wrapper_id #wrapper_ty_generics #wrapper_where_clause {
#[inline]
fn write_into<AskamaW>(&self, dest: &mut AskamaW) -> askama::Result<()>
fn write_into<AskamaW>(
&self,
dest: &mut AskamaW,
values: &dyn askama::Values,
) -> askama::Result<()>
where
AskamaW: askama::helpers::core::fmt::Write + ?askama::helpers::core::marker::Sized
{
<_ as askama::Template>::render_into(self, dest)
<_ as askama::Template>::render_into_with_values(self, dest, values)
}
}

View File

@ -268,6 +268,7 @@ impl<'a> Generator<'a, '_> {
"urlencode" => Self::_visit_urlencode_filter,
"urlencode_strict" => Self::_visit_urlencode_strict_filter,
"value" => return self._visit_value(ctx, buf, args, generics, node, "`value` filter"),
"wordcount" => Self::_visit_wordcount_filter,
name if BUILTIN_FILTERS.contains(&name) => {
return self._visit_builtin_filter(ctx, buf, name, args, generics, node);
}
@ -295,7 +296,12 @@ impl<'a> Generator<'a, '_> {
buf.write(format_args!("filters::{name}"));
self.visit_call_generics(buf, generics);
buf.write('(');
self._visit_args(ctx, buf, args)?;
self._visit_arg(ctx, buf, &args[0])?;
buf.write(",__askama_values");
if args.len() > 1 {
buf.write(',');
self._visit_args(ctx, buf, &args[1..])?;
}
buf.write(")?");
Ok(DisplayWrap::Unwrapped)
}
@ -366,6 +372,37 @@ impl<'a> Generator<'a, '_> {
Ok(DisplayWrap::Unwrapped)
}
fn _visit_wordcount_filter(
&mut self,
ctx: &Context<'_>,
buf: &mut Buffer,
args: &[WithSpan<'_, Expr<'a>>],
node: Span<'_>,
) -> Result<DisplayWrap, CompileError> {
ensure_filter_has_feature_alloc(ctx, "wordcount", node)?;
if args.len() != 1 {
return Err(ctx.generate_error(
format_args!("unexpected argument(s) in `wordcount` filter"),
node,
));
}
buf.write("match askama::filters::wordcount(&(");
self._visit_args(ctx, buf, args)?;
buf.write(
")) {\
expr0 => {\
(&&&askama::filters::Writable(&expr0)).\
askama_write(&mut askama::helpers::Empty, __askama_values)?;\
expr0.into_count()\
}\
}\
",
);
Ok(DisplayWrap::Unwrapped)
}
fn _visit_humansize(
&mut self,
ctx: &Context<'_>,

View File

@ -64,11 +64,15 @@ fn impl_fast_writable(ast: &DeriveInput, buf: &mut Buffer) {
buf.write(
"\
#[inline]\
fn write_into<AskamaW>(&self, dest: &mut AskamaW) -> askama::Result<()> \
fn write_into<AskamaW>(\
&self,\
dest: &mut AskamaW,\
values: &dyn askama::Values\
) -> askama::Result<()> \
where \
AskamaW: askama::helpers::core::fmt::Write + ?askama::helpers::core::marker::Sized,\
{\
askama::Template::render_into(self, dest)\
askama::Template::render_into_with_values(self, dest, values)\
}\
}",
);

View File

@ -611,7 +611,6 @@ const BUILTIN_FILTERS: &[&str] = &[
"truncate",
"upper",
"uppercase",
"wordcount",
];
// Built-in filters that need the `alloc` feature.

View File

@ -69,11 +69,15 @@ fn compare_ex(
impl askama::filters::FastWritable for Foo {
#[inline]
fn write_into<AskamaW>(&self, dest: &mut AskamaW) -> askama::Result<()>
fn write_into<AskamaW>(
&self,
dest: &mut AskamaW,
values: &dyn askama::Values,
) -> askama::Result<()>
where
AskamaW: askama::helpers::core::fmt::Write + ?askama::helpers::core::marker::Sized,
{
askama::Template::render_into(self, dest)
askama::Template::render_into_with_values(self, dest, values)
}
}
};

View File

@ -467,17 +467,20 @@ Prefix with two &nbsp; characters:
To define your own filters, simply have a module named `filters` in scope of the context deriving a `Template` impl
and define the filters as functions within this module.
The functions must have at least one argument and the return type must be `askama::Result<T>`.
The functions must have at least two arguments and the return type must be `askama::Result<T>`.
Although there are no restrictions on `T` for a single filter,
the final result of a chain of filters must implement `Display`.
The arguments to the filters are passed as follows.
The first argument corresponds to the expression they are applied to.
Subsequent arguments, if any, must be given directly when calling the filter.
The first argument may or may not be a reference, depending on the context in which the filter is called.
The arguments to the filters are passed as follows:
* The first argument corresponds to the expression they are applied to.
* The second argument are the [runtime values](./runtime.html):
[`values: &dyn askama::Values`](./doc/askama/trait.Values.html).
* Subsequent arguments, if any, must be given directly when calling the filter.
The first argument may or may not be a reference, depending on the context in which the filter is called.
To abstract over ownership, consider defining your argument as a trait bound.
For example, the `trim` built-in filter accepts any value implementing `Display`.
Its signature is similar to `fn trim(s: impl std::fmt::Display) -> askama::Result<String>`.
Its signature is similar to `fn trim(s: impl std::fmt::Display, values: &dyn askama::Values) -> askama::Result<String>`.
Note that built-in filters have preference over custom filters, so, in case of name collision, the built-in filter is applied.
@ -496,7 +499,10 @@ struct MyFilterTemplate<'a> {
// Any filter defined in the module `filters` is accessible in your template.
mod filters {
// This filter does not have extra arguments
pub fn myfilter<T: std::fmt::Display>(s: T) -> askama::Result<String> {
pub fn myfilter<T: std::fmt::Display>(
s: T,
_: &dyn askama::Values,
) -> askama::Result<String> {
let s = s.to_string();
Ok(s.replace("oo", "aa"))
}
@ -521,7 +527,11 @@ struct MyFilterTemplate<'a> {
// Any filter defined in the module `filters` is accessible in your template.
mod filters {
// This filter requires a `usize` input when called in templates
pub fn myfilter<T: std::fmt::Display>(s: T, n: usize) -> askama::Result<String> {
pub fn myfilter<T: std::fmt::Display>(
s: T,
_: &dyn askama::Values,
n: usize,
) -> askama::Result<String> {
let s = s.to_string();
let mut replace = String::with_capacity(n);
replace.extend((0..n).map(|_| "a"));
@ -535,6 +545,15 @@ fn main() {
}
```
### Runtime values
[#runtime-values]: #runtime-values
It is possible to access [runtime values](./runtime.html) in custom filters:
```rust
{{#include ../../testing/tests/book_example_runtime_values_in_custom_filters.rs}}
```
## HTML-safe types
[#html-safe-types]: #html-safe-types

View File

@ -0,0 +1,60 @@
// This example contains a custom filter `|cased`.
// Depending on the runtime value `"case"`, the input is either turned
// into lower case, upper case, or left alone, if the runtime value is undefined.
use std::any::Any;
use askama::{Template, Values};
mod filters {
use super::*;
pub fn cased(value: impl ToString, values: &dyn Values) -> askama::Result<String> {
let value = value.to_string();
let case = askama::get_value(values, "case").ok();
Ok(match case {
Some(Case::Lower) => value.to_lowercase(),
Some(Case::Upper) => value.to_uppercase(),
None => value,
})
}
}
#[derive(Debug, Clone, Copy)]
pub enum Case {
Lower,
Upper,
}
#[test]
fn test_runtime_values_in_custom_filters() {
#[derive(Template)]
#[template(ext = "txt", source = "Hello, {{ user | cased }}!")]
struct MyStruct<'a> {
user: &'a str,
}
// The filter source ("wOrLd") should be written in lower case.
let values: (&str, &dyn Any) = ("case", &Case::Lower);
assert_eq!(
MyStruct { user: "wOrLd" }
.render_with_values(&values)
.unwrap(),
"Hello, world!"
);
// The filter source ("wOrLd") should be written in upper case.
let values: (&str, &dyn Any) = ("case", &Case::Upper);
assert_eq!(
MyStruct { user: "wOrLd" }
.render_with_values(&values)
.unwrap(),
"Hello, WORLD!"
);
// The filter source ("wOrLd") should be written as is.
assert_eq!(
MyStruct { user: "wOrLd" }.render().unwrap(),
"Hello, wOrLd!"
);
}

View File

@ -99,11 +99,14 @@ fn filter_fmt() {
}
mod filters {
pub fn myfilter(s: &str) -> ::askama::Result<String> {
use askama::Values;
pub fn myfilter(s: &str, _: &dyn Values) -> ::askama::Result<String> {
Ok(s.replace("oo", "aa"))
}
// for test_nested_filter_ref
pub fn mytrim(s: &dyn (::std::fmt::Display)) -> ::askama::Result<String> {
pub fn mytrim(s: &dyn std::fmt::Display, _: &dyn Values) -> ::askama::Result<String> {
Ok(s.to_string().trim().to_owned())
}
}

View File

@ -88,3 +88,71 @@ fn test_value_function_getter() {
values.insert("a".to_string(), Box::new(false));
assert_eq!(V.render_with_values(&values).unwrap(), "");
}
#[test]
fn test_value_in_subtemplates() {
// In this test we make sure that values are passed down to transcluded sub-templates,
// even if there is a filter in the mix, e.g. the implicit `|escape` filter.
#[derive(Template)]
#[template(source = r#"{{ Child }}"#, ext = "html")]
struct Parent;
#[derive(Template)]
#[template(
source = r#"Hello, {{ askama::get_value::<String>("who")? }}!"#,
ext = "html"
)]
struct Child;
let values: (&str, &dyn Any) = ("who", &"<world>".to_owned());
assert_eq!(
Parent.render_with_values(&values).unwrap(),
"Hello, &#38;#60;world&#38;#62;!", // sic: escaped twice
);
}
#[test]
fn test_value_in_subtemplates_with_filters() {
// In this test we make sure that values are passed down to transcluded sub-templates,
// even if there is a filter in the mix.
#[derive(Template)]
#[template(source = r#"{{ Child }}"#, ext = "html")]
struct Parent;
#[derive(Template)]
#[template(
source = r#"Hello, {{ askama::get_value::<String>("who")? | upper }}!"#,
ext = "html"
)]
struct Child;
let values: (&str, &dyn Any) = ("who", &"<world>".to_owned());
assert_eq!(
Parent.render_with_values(&values).unwrap(),
"Hello, &#38;#60;WORLD&#38;#62;!", // sic: escaped twice
);
}
#[test]
fn test_value_in_wordcount() {
// This test makes sure that `|wordcount` has has access to runtime values.
#[derive(Template)]
#[template(source = r#"{{ Child|wordcount }}"#, ext = "html")]
struct Parent;
#[derive(Template)]
#[template(
source = r#"{{ askama::get_value::<&str>("sentence")? }}"#,
ext = "html"
)]
struct Child;
let values: (&str, &dyn Any) = (
"sentence",
&"In simple terms, a sentence is a set of words.",
);
assert_eq!(Parent.render_with_values(&values).unwrap(), "10");
}