770 lines
24 KiB
Rust
770 lines
24 KiB
Rust
/*
|
|
* This file is part of mailpot
|
|
*
|
|
* Copyright 2020 - Manos Pitsidianakis
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
* License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
//! Utils for templates with the [`minijinja`] crate.
|
|
|
|
use super::*;
|
|
|
|
mod compressed;
|
|
|
|
lazy_static::lazy_static! {
|
|
pub static ref TEMPLATES: Environment<'static> = {
|
|
let mut env = Environment::new();
|
|
macro_rules! add {
|
|
(function $($id:ident),*$(,)?) => {
|
|
$(env.add_function(stringify!($id), $id);)*
|
|
};
|
|
(filter $($id:ident),*$(,)?) => {
|
|
$(env.add_filter(stringify!($id), $id);)*
|
|
}
|
|
}
|
|
add!(function calendarize,
|
|
strip_carets,
|
|
urlize,
|
|
heading,
|
|
login_path,
|
|
logout_path,
|
|
settings_path,
|
|
help_path,
|
|
list_path,
|
|
list_settings_path,
|
|
list_edit_path,
|
|
list_subscribers_path,
|
|
list_candidates_path,
|
|
list_post_path,
|
|
post_raw_path,
|
|
post_eml_path
|
|
);
|
|
add!(filter pluralize);
|
|
// Load compressed templates. They are constructed in build.rs. See
|
|
// [ref:embed_templates]
|
|
let mut source = minijinja::Source::new();
|
|
for (name, bytes) in compressed::COMPRESSED {
|
|
let mut de_bytes = vec![];
|
|
zstd::stream::copy_decode(*bytes,&mut de_bytes).unwrap();
|
|
source.add_template(*name, String::from_utf8(de_bytes).unwrap()).unwrap();
|
|
}
|
|
env.set_source(source);
|
|
|
|
env.add_global("root_url_prefix", Value::from_safe_string( std::env::var("ROOT_URL_PREFIX").unwrap_or_default()));
|
|
env.add_global("public_url",Value::from_safe_string(std::env::var("PUBLIC_URL").unwrap_or_default()));
|
|
env.add_global("site_title", Value::from_safe_string(std::env::var("SITE_TITLE").unwrap_or_else(|_| "mailing list archive".to_string())));
|
|
env.add_global("site_subtitle", std::env::var("SITE_SUBTITLE").ok().map(Value::from_safe_string).unwrap_or_default());
|
|
|
|
env
|
|
};
|
|
}
|
|
|
|
pub trait StripCarets {
|
|
fn strip_carets(&self) -> &str;
|
|
}
|
|
|
|
impl StripCarets for &str {
|
|
fn strip_carets(&self) -> &str {
|
|
let mut self_ref = self.trim();
|
|
if self_ref.starts_with('<') && self_ref.ends_with('>') {
|
|
self_ref = &self_ref[1..self_ref.len().saturating_sub(1)];
|
|
}
|
|
self_ref
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize)]
|
|
pub struct MailingList {
|
|
pub pk: i64,
|
|
pub name: String,
|
|
pub id: String,
|
|
pub address: String,
|
|
pub description: Option<String>,
|
|
pub topics: Vec<String>,
|
|
#[serde(serialize_with = "super::utils::to_safe_string_opt")]
|
|
pub archive_url: Option<String>,
|
|
pub inner: DbVal<mailpot::models::MailingList>,
|
|
}
|
|
|
|
impl From<DbVal<mailpot::models::MailingList>> for MailingList {
|
|
fn from(val: DbVal<mailpot::models::MailingList>) -> Self {
|
|
let DbVal(
|
|
mailpot::models::MailingList {
|
|
pk,
|
|
name,
|
|
id,
|
|
address,
|
|
description,
|
|
topics,
|
|
archive_url,
|
|
},
|
|
_,
|
|
) = val.clone();
|
|
|
|
Self {
|
|
pk,
|
|
name,
|
|
id,
|
|
address,
|
|
description,
|
|
topics,
|
|
archive_url,
|
|
inner: val,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for MailingList {
|
|
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
self.id.fmt(fmt)
|
|
}
|
|
}
|
|
|
|
impl Object for MailingList {
|
|
fn kind(&self) -> minijinja::value::ObjectKind {
|
|
minijinja::value::ObjectKind::Struct(self)
|
|
}
|
|
|
|
fn call_method(
|
|
&self,
|
|
_state: &minijinja::State,
|
|
name: &str,
|
|
_args: &[Value],
|
|
) -> std::result::Result<Value, Error> {
|
|
match name {
|
|
"subscription_mailto" => {
|
|
Ok(Value::from_serializable(&self.inner.subscription_mailto()))
|
|
}
|
|
"unsubscription_mailto" => Ok(Value::from_serializable(
|
|
&self.inner.unsubscription_mailto(),
|
|
)),
|
|
_ => Err(Error::new(
|
|
minijinja::ErrorKind::UnknownMethod,
|
|
format!("object has no method named {name}"),
|
|
)),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl minijinja::value::StructObject for MailingList {
|
|
fn get_field(&self, name: &str) -> Option<Value> {
|
|
match name {
|
|
"pk" => Some(Value::from_serializable(&self.pk)),
|
|
"name" => Some(Value::from_serializable(&self.name)),
|
|
"id" => Some(Value::from_serializable(&self.id)),
|
|
"address" => Some(Value::from_serializable(&self.address)),
|
|
"description" => Some(Value::from_serializable(&self.description)),
|
|
"topics" => Some(Value::from_serializable(&self.topics)),
|
|
"archive_url" => Some(Value::from_serializable(&self.archive_url)),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn static_fields(&self) -> Option<&'static [&'static str]> {
|
|
Some(
|
|
&[
|
|
"pk",
|
|
"name",
|
|
"id",
|
|
"address",
|
|
"description",
|
|
"topics",
|
|
"archive_url",
|
|
][..],
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Return a vector of weeks, with each week being a vector of 7 days and
|
|
/// corresponding sum of posts per day.
|
|
pub fn calendarize(
|
|
_state: &minijinja::State,
|
|
args: Value,
|
|
hists: Value,
|
|
) -> std::result::Result<Value, Error> {
|
|
use chrono::Month;
|
|
|
|
macro_rules! month {
|
|
($int:expr) => {{
|
|
let int = $int;
|
|
match int {
|
|
1 => Month::January.name(),
|
|
2 => Month::February.name(),
|
|
3 => Month::March.name(),
|
|
4 => Month::April.name(),
|
|
5 => Month::May.name(),
|
|
6 => Month::June.name(),
|
|
7 => Month::July.name(),
|
|
8 => Month::August.name(),
|
|
9 => Month::September.name(),
|
|
10 => Month::October.name(),
|
|
11 => Month::November.name(),
|
|
12 => Month::December.name(),
|
|
_ => unreachable!(),
|
|
}
|
|
}};
|
|
}
|
|
let month = args.as_str().unwrap();
|
|
let hist = hists
|
|
.get_item(&Value::from(month))?
|
|
.as_seq()
|
|
.unwrap()
|
|
.iter()
|
|
.map(|v| usize::try_from(v).unwrap())
|
|
.collect::<Vec<usize>>();
|
|
let sum: usize = hists
|
|
.get_item(&Value::from(month))?
|
|
.as_seq()
|
|
.unwrap()
|
|
.iter()
|
|
.map(|v| usize::try_from(v).unwrap())
|
|
.sum();
|
|
let date = chrono::NaiveDate::parse_from_str(&format!("{}-01", month), "%F").unwrap();
|
|
// Week = [Mon, Tue, Wed, Thu, Fri, Sat, Sun]
|
|
Ok(minijinja::context! {
|
|
month_name => month!(date.month()),
|
|
month => month,
|
|
month_int => date.month() as usize,
|
|
year => date.year(),
|
|
weeks => cal::calendarize_with_offset(date, 1),
|
|
hist => hist,
|
|
sum,
|
|
})
|
|
}
|
|
|
|
/// `pluralize` filter for [`minijinja`].
|
|
///
|
|
/// Returns a plural suffix if the value is not `1`, `"1"`, or an object of
|
|
/// length `1`. By default, the plural suffix is 's' and the singular suffix is
|
|
/// empty (''). You can specify a singular suffix as the first argument (or
|
|
/// `None`, for the default). You can specify a plural suffix as the second
|
|
/// argument (or `None`, for the default).
|
|
///
|
|
/// See the examples for the correct usage.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```rust,no_run
|
|
/// # use mailpot_web::pluralize;
|
|
/// # use minijinja::Environment;
|
|
///
|
|
/// let mut env = Environment::new();
|
|
/// env.add_filter("pluralize", pluralize);
|
|
/// for (num, s) in [
|
|
/// (0, "You have 0 messages."),
|
|
/// (1, "You have 1 message."),
|
|
/// (10, "You have 10 messages."),
|
|
/// ] {
|
|
/// assert_eq!(
|
|
/// &env.render_str(
|
|
/// "You have {{ num_messages }} message{{ num_messages|pluralize }}.",
|
|
/// minijinja::context! {
|
|
/// num_messages => num,
|
|
/// }
|
|
/// )
|
|
/// .unwrap(),
|
|
/// s
|
|
/// );
|
|
/// }
|
|
///
|
|
/// for (num, s) in [
|
|
/// (0, "You have 0 walruses."),
|
|
/// (1, "You have 1 walrus."),
|
|
/// (10, "You have 10 walruses."),
|
|
/// ] {
|
|
/// assert_eq!(
|
|
/// &env.render_str(
|
|
/// r#"You have {{ num_walruses }} walrus{{ num_walruses|pluralize(None, "es") }}."#,
|
|
/// minijinja::context! {
|
|
/// num_walruses => num,
|
|
/// }
|
|
/// )
|
|
/// .unwrap(),
|
|
/// s
|
|
/// );
|
|
/// }
|
|
///
|
|
/// for (num, s) in [
|
|
/// (0, "You have 0 cherries."),
|
|
/// (1, "You have 1 cherry."),
|
|
/// (10, "You have 10 cherries."),
|
|
/// ] {
|
|
/// assert_eq!(
|
|
/// &env.render_str(
|
|
/// r#"You have {{ num_cherries }} cherr{{ num_cherries|pluralize("y", "ies") }}."#,
|
|
/// minijinja::context! {
|
|
/// num_cherries => num,
|
|
/// }
|
|
/// )
|
|
/// .unwrap(),
|
|
/// s
|
|
/// );
|
|
/// }
|
|
///
|
|
/// assert_eq!(
|
|
/// &env.render_str(
|
|
/// r#"You have {{ num_cherries|length }} cherr{{ num_cherries|pluralize("y", "ies") }}."#,
|
|
/// minijinja::context! {
|
|
/// num_cherries => vec![(); 5],
|
|
/// }
|
|
/// )
|
|
/// .unwrap(),
|
|
/// "You have 5 cherries."
|
|
/// );
|
|
///
|
|
/// assert_eq!(
|
|
/// &env.render_str(
|
|
/// r#"You have {{ num_cherries }} cherr{{ num_cherries|pluralize("y", "ies") }}."#,
|
|
/// minijinja::context! {
|
|
/// num_cherries => "5",
|
|
/// }
|
|
/// )
|
|
/// .unwrap(),
|
|
/// "You have 5 cherries."
|
|
/// );
|
|
/// assert_eq!(
|
|
/// &env.render_str(
|
|
/// r#"You have 1 cherr{{ num_cherries|pluralize("y", "ies") }}."#,
|
|
/// minijinja::context! {
|
|
/// num_cherries => true,
|
|
/// }
|
|
/// )
|
|
/// .unwrap()
|
|
/// .to_string(),
|
|
/// "You have 1 cherry.",
|
|
/// );
|
|
/// assert_eq!(
|
|
/// &env.render_str(
|
|
/// r#"You have {{ num_cherries }} cherr{{ num_cherries|pluralize("y", "ies") }}."#,
|
|
/// minijinja::context! {
|
|
/// num_cherries => 0.5f32,
|
|
/// }
|
|
/// )
|
|
/// .unwrap_err()
|
|
/// .to_string(),
|
|
/// "invalid operation: Pluralize argument is not an integer, or a sequence / object with a \
|
|
/// length but of type number (in <string>:1)",
|
|
/// );
|
|
/// ```
|
|
pub fn pluralize(
|
|
v: Value,
|
|
singular: Option<String>,
|
|
plural: Option<String>,
|
|
) -> Result<Value, minijinja::Error> {
|
|
macro_rules! int_try_from {
|
|
($ty:ty) => {
|
|
<$ty>::try_from(v.clone()).ok().map(|v| v != 1)
|
|
};
|
|
($fty:ty, $($ty:ty),*) => {
|
|
int_try_from!($fty).or_else(|| int_try_from!($($ty),*))
|
|
}
|
|
}
|
|
let is_plural: bool = v
|
|
.as_str()
|
|
.and_then(|s| s.parse::<i128>().ok())
|
|
.map(|l| l != 1)
|
|
.or_else(|| v.len().map(|l| l != 1))
|
|
.or_else(|| int_try_from!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, usize))
|
|
.ok_or_else(|| {
|
|
minijinja::Error::new(
|
|
minijinja::ErrorKind::InvalidOperation,
|
|
format!(
|
|
"Pluralize argument is not an integer, or a sequence / object with a length \
|
|
but of type {}",
|
|
v.kind()
|
|
),
|
|
)
|
|
})?;
|
|
Ok(match (is_plural, singular, plural) {
|
|
(false, None, _) => "".into(),
|
|
(false, Some(suffix), _) => suffix.into(),
|
|
(true, _, None) => "s".into(),
|
|
(true, _, Some(suffix)) => suffix.into(),
|
|
})
|
|
}
|
|
|
|
/// `strip_carets` filter for [`minijinja`].
|
|
///
|
|
/// Removes `[<>]` from message ids.
|
|
pub fn strip_carets(_state: &minijinja::State, arg: Value) -> std::result::Result<Value, Error> {
|
|
Ok(Value::from(
|
|
arg.as_str()
|
|
.ok_or_else(|| {
|
|
minijinja::Error::new(
|
|
minijinja::ErrorKind::InvalidOperation,
|
|
format!("argument to strip_carets() is of type {}", arg.kind()),
|
|
)
|
|
})?
|
|
.strip_carets(),
|
|
))
|
|
}
|
|
|
|
/// `urlize` filter for [`minijinja`].
|
|
///
|
|
/// Returns a safe string for use in `<a href=..` attributes.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```rust,no_run
|
|
/// # use mailpot_web::urlize;
|
|
/// # use minijinja::Environment;
|
|
/// # use minijinja::value::Value;
|
|
///
|
|
/// let mut env = Environment::new();
|
|
/// env.add_function("urlize", urlize);
|
|
/// env.add_global(
|
|
/// "root_url_prefix",
|
|
/// Value::from_safe_string("/lists/prefix/".to_string()),
|
|
/// );
|
|
/// assert_eq!(
|
|
/// &env.render_str(
|
|
/// "<a href=\"{{ urlize(\"path/index.html\") }}\">link</a>",
|
|
/// minijinja::context! {}
|
|
/// )
|
|
/// .unwrap(),
|
|
/// "<a href=\"/lists/prefix/path/index.html\">link</a>",
|
|
/// );
|
|
/// ```
|
|
pub fn urlize(state: &minijinja::State, arg: Value) -> std::result::Result<Value, Error> {
|
|
let Some(prefix) = state.lookup("root_url_prefix") else {
|
|
return Ok(arg);
|
|
};
|
|
Ok(Value::from_safe_string(format!("{prefix}{arg}")))
|
|
}
|
|
|
|
/// Make an html heading: `h1, h2, h3` etc.
|
|
///
|
|
/// # Example
|
|
/// ```rust,no_run
|
|
/// use mailpot_web::minijinja_utils::heading;
|
|
/// use minijinja::value::Value;
|
|
///
|
|
/// assert_eq!(
|
|
/// "<h1 id=\"bl-bfa-b-ah-b-asdb-hadas-d\">bl bfa B AH bAsdb hadas d<a class=\"self-link\" href=\"#bl-bfa-b-ah-b-asdb-hadas-d\"></a></h1>",
|
|
/// &heading(1.into(), "bl bfa B AH bAsdb hadas d".into(), None).unwrap().to_string()
|
|
/// );
|
|
/// assert_eq!(
|
|
/// "<h2 id=\"short\">bl bfa B AH bAsdb hadas d<a class=\"self-link\" href=\"#short\"></a></h2>",
|
|
/// &heading(2.into(), "bl bfa B AH bAsdb hadas d".into(), Some("short".into())).unwrap().to_string()
|
|
/// );
|
|
/// assert_eq!(
|
|
/// r#"invalid operation: first heading() argument must be an unsigned integer less than 7 and positive"#,
|
|
/// &heading(0.into(), "bl bfa B AH bAsdb hadas d".into(), Some("short".into())).unwrap_err().to_string()
|
|
/// );
|
|
/// assert_eq!(
|
|
/// r#"invalid operation: first heading() argument must be an unsigned integer less than 7 and positive"#,
|
|
/// &heading(8.into(), "bl bfa B AH bAsdb hadas d".into(), Some("short".into())).unwrap_err().to_string()
|
|
/// );
|
|
/// assert_eq!(
|
|
/// r#"invalid operation: first heading() argument is not an integer < 7 but of type sequence"#,
|
|
/// &heading(Value::from(vec![Value::from(1)]), "bl bfa B AH bAsdb hadas d".into(), Some("short".into())).unwrap_err().to_string()
|
|
/// );
|
|
/// ```
|
|
pub fn heading(level: Value, text: Value, id: Option<Value>) -> std::result::Result<Value, Error> {
|
|
use convert_case::{Case, Casing};
|
|
macro_rules! test {
|
|
() => {
|
|
|n| *n > 0 && *n < 7
|
|
};
|
|
}
|
|
|
|
macro_rules! int_try_from {
|
|
($ty:ty) => {
|
|
<$ty>::try_from(level.clone()).ok().filter(test!{}).map(|n| n as u8)
|
|
};
|
|
($fty:ty, $($ty:ty),*) => {
|
|
int_try_from!($fty).or_else(|| int_try_from!($($ty),*))
|
|
}
|
|
}
|
|
let level: u8 = level
|
|
.as_str()
|
|
.and_then(|s| s.parse::<i128>().ok())
|
|
.filter(test! {})
|
|
.map(|n| n as u8)
|
|
.or_else(|| int_try_from!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, usize))
|
|
.ok_or_else(|| {
|
|
if matches!(level.kind(), minijinja::value::ValueKind::Number) {
|
|
minijinja::Error::new(
|
|
minijinja::ErrorKind::InvalidOperation,
|
|
"first heading() argument must be an unsigned integer less than 7 and positive",
|
|
)
|
|
} else {
|
|
minijinja::Error::new(
|
|
minijinja::ErrorKind::InvalidOperation,
|
|
format!(
|
|
"first heading() argument is not an integer < 7 but of type {}",
|
|
level.kind()
|
|
),
|
|
)
|
|
}
|
|
})?;
|
|
let text = text.as_str().ok_or_else(|| {
|
|
minijinja::Error::new(
|
|
minijinja::ErrorKind::InvalidOperation,
|
|
format!(
|
|
"second heading() argument is not a string but of type {}",
|
|
text.kind()
|
|
),
|
|
)
|
|
})?;
|
|
if let Some(v) = id {
|
|
let kebab = v.as_str().ok_or_else(|| {
|
|
minijinja::Error::new(
|
|
minijinja::ErrorKind::InvalidOperation,
|
|
format!(
|
|
"third heading() argument is not a string but of type {}",
|
|
v.kind()
|
|
),
|
|
)
|
|
})?;
|
|
Ok(Value::from_safe_string(format!(
|
|
"<h{level} id=\"{kebab}\">{text}<a class=\"self-link\" \
|
|
href=\"#{kebab}\"></a></h{level}>"
|
|
)))
|
|
} else {
|
|
let kebab_v = text.to_case(Case::Kebab);
|
|
let kebab =
|
|
percent_encoding::utf8_percent_encode(&kebab_v, crate::typed_paths::PATH_SEGMENT);
|
|
Ok(Value::from_safe_string(format!(
|
|
"<h{level} id=\"{kebab}\">{text}<a class=\"self-link\" \
|
|
href=\"#{kebab}\"></a></h{level}>"
|
|
)))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_pluralize() {
|
|
let mut env = Environment::new();
|
|
env.add_filter("pluralize", pluralize);
|
|
for (num, s) in [
|
|
(0, "You have 0 messages."),
|
|
(1, "You have 1 message."),
|
|
(10, "You have 10 messages."),
|
|
] {
|
|
assert_eq!(
|
|
&env.render_str(
|
|
"You have {{ num_messages }} message{{ num_messages|pluralize }}.",
|
|
minijinja::context! {
|
|
num_messages => num,
|
|
}
|
|
)
|
|
.unwrap(),
|
|
s
|
|
);
|
|
}
|
|
|
|
for (num, s) in [
|
|
(0, "You have 0 walruses."),
|
|
(1, "You have 1 walrus."),
|
|
(10, "You have 10 walruses."),
|
|
] {
|
|
assert_eq!(
|
|
&env.render_str(
|
|
r#"You have {{ num_walruses }} walrus{{ num_walruses|pluralize(None, "es") }}."#,
|
|
minijinja::context! {
|
|
num_walruses => num,
|
|
}
|
|
)
|
|
.unwrap(),
|
|
s
|
|
);
|
|
}
|
|
|
|
for (num, s) in [
|
|
(0, "You have 0 cherries."),
|
|
(1, "You have 1 cherry."),
|
|
(10, "You have 10 cherries."),
|
|
] {
|
|
assert_eq!(
|
|
&env.render_str(
|
|
r#"You have {{ num_cherries }} cherr{{ num_cherries|pluralize("y", "ies") }}."#,
|
|
minijinja::context! {
|
|
num_cherries => num,
|
|
}
|
|
)
|
|
.unwrap(),
|
|
s
|
|
);
|
|
}
|
|
|
|
assert_eq!(
|
|
&env.render_str(
|
|
r#"You have {{ num_cherries|length }} cherr{{ num_cherries|pluralize("y", "ies") }}."#,
|
|
minijinja::context! {
|
|
num_cherries => vec![(); 5],
|
|
}
|
|
)
|
|
.unwrap(),
|
|
"You have 5 cherries."
|
|
);
|
|
|
|
assert_eq!(
|
|
&env.render_str(
|
|
r#"You have {{ num_cherries }} cherr{{ num_cherries|pluralize("y", "ies") }}."#,
|
|
minijinja::context! {
|
|
num_cherries => "5",
|
|
}
|
|
)
|
|
.unwrap(),
|
|
"You have 5 cherries."
|
|
);
|
|
assert_eq!(
|
|
&env.render_str(
|
|
r#"You have 1 cherr{{ num_cherries|pluralize("y", "ies") }}."#,
|
|
minijinja::context! {
|
|
num_cherries => true,
|
|
}
|
|
)
|
|
.unwrap(),
|
|
"You have 1 cherry.",
|
|
);
|
|
assert_eq!(
|
|
&env.render_str(
|
|
r#"You have {{ num_cherries }} cherr{{ num_cherries|pluralize("y", "ies") }}."#,
|
|
minijinja::context! {
|
|
num_cherries => 0.5f32,
|
|
}
|
|
)
|
|
.unwrap_err()
|
|
.to_string(),
|
|
"invalid operation: Pluralize argument is not an integer, or a sequence / object with \
|
|
a length but of type number (in <string>:1)",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_urlize() {
|
|
let mut env = Environment::new();
|
|
env.add_function("urlize", urlize);
|
|
env.add_global(
|
|
"root_url_prefix",
|
|
Value::from_safe_string("/lists/prefix/".to_string()),
|
|
);
|
|
assert_eq!(
|
|
&env.render_str(
|
|
"<a href=\"{{ urlize(\"path/index.html\") }}\">link</a>",
|
|
minijinja::context! {}
|
|
)
|
|
.unwrap(),
|
|
"<a href=\"/lists/prefix/path/index.html\">link</a>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_heading() {
|
|
assert_eq!(
|
|
"<h1 id=\"bl-bfa-b-ah-b-asdb-hadas-d\">bl bfa B AH bAsdb hadas d<a \
|
|
class=\"self-link\" href=\"#bl-bfa-b-ah-b-asdb-hadas-d\"></a></h1>",
|
|
&heading(1.into(), "bl bfa B AH bAsdb hadas d".into(), None)
|
|
.unwrap()
|
|
.to_string()
|
|
);
|
|
assert_eq!(
|
|
"<h2 id=\"short\">bl bfa B AH bAsdb hadas d<a class=\"self-link\" \
|
|
href=\"#short\"></a></h2>",
|
|
&heading(
|
|
2.into(),
|
|
"bl bfa B AH bAsdb hadas d".into(),
|
|
Some("short".into())
|
|
)
|
|
.unwrap()
|
|
.to_string()
|
|
);
|
|
assert_eq!(
|
|
r#"invalid operation: first heading() argument must be an unsigned integer less than 7 and positive"#,
|
|
&heading(
|
|
0.into(),
|
|
"bl bfa B AH bAsdb hadas d".into(),
|
|
Some("short".into())
|
|
)
|
|
.unwrap_err()
|
|
.to_string()
|
|
);
|
|
assert_eq!(
|
|
r#"invalid operation: first heading() argument must be an unsigned integer less than 7 and positive"#,
|
|
&heading(
|
|
8.into(),
|
|
"bl bfa B AH bAsdb hadas d".into(),
|
|
Some("short".into())
|
|
)
|
|
.unwrap_err()
|
|
.to_string()
|
|
);
|
|
assert_eq!(
|
|
r#"invalid operation: first heading() argument is not an integer < 7 but of type sequence"#,
|
|
&heading(
|
|
Value::from(vec![Value::from(1)]),
|
|
"bl bfa B AH bAsdb hadas d".into(),
|
|
Some("short".into())
|
|
)
|
|
.unwrap_err()
|
|
.to_string()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_strip_carets() {
|
|
let mut env = Environment::new();
|
|
env.add_filter("strip_carets", strip_carets);
|
|
assert_eq!(
|
|
&env.render_str(
|
|
"{{ msg_id | strip_carets }}",
|
|
minijinja::context! {
|
|
msg_id => "<hello1@example.com>",
|
|
}
|
|
)
|
|
.unwrap(),
|
|
"hello1@example.com",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_calendarize() {
|
|
use std::collections::HashMap;
|
|
|
|
let mut env = Environment::new();
|
|
env.add_function("calendarize", calendarize);
|
|
|
|
let month = "2001-09";
|
|
let mut hist = [0usize; 31];
|
|
hist[15] = 5;
|
|
hist[1] = 1;
|
|
hist[0] = 512;
|
|
hist[30] = 30;
|
|
assert_eq!(
|
|
&env.render_str(
|
|
"{% set c=calendarize(month, hists) %}Month: {{ c.month }} Month Name: {{ \
|
|
c.month_name }} Month Int: {{ c.month_int }} Year: {{ c.year }} Sum: {{ c.sum }} {% \
|
|
for week in c.weeks %}{% for day in week %}{% set num = c.hist[day-1] %}({{ day }}, \
|
|
{{ num }}){% endfor %}{% endfor %}",
|
|
minijinja::context! {
|
|
month,
|
|
hists => vec![(month.to_string(), hist)].into_iter().collect::<HashMap<String, [usize;
|
|
31]>>(),
|
|
}
|
|
)
|
|
.unwrap(),
|
|
"Month: 2001-09 Month Name: September Month Int: 9 Year: 2001 Sum: 548 (0, 30)(0, 30)(0, \
|
|
30)(0, 30)(0, 30)(1, 512)(2, 1)(3, 0)(4, 0)(5, 0)(6, 0)(7, 0)(8, 0)(9, 0)(10, 0)(11, \
|
|
0)(12, 0)(13, 0)(14, 0)(15, 0)(16, 5)(17, 0)(18, 0)(19, 0)(20, 0)(21, 0)(22, 0)(23, \
|
|
0)(24, 0)(25, 0)(26, 0)(27, 0)(28, 0)(29, 0)(30, 0)"
|
|
);
|
|
}
|
|
}
|