1
/*
2
 * This file is part of mailpot
3
 *
4
 * Copyright 2020 - Manos Pitsidianakis
5
 *
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU Affero General Public License as
8
 * published by the Free Software Foundation, either version 3 of the
9
 * License, or (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU Affero General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU Affero General Public License
17
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18
 */
19

            
20
//! Utils for templates with the [`minijinja`] crate.
21

            
22
use super::*;
23

            
24
#[cfg(feature = "zstd")]
25
#[cfg(not(debug_assertions))]
26
mod compressed;
27

            
28
lazy_static::lazy_static! {
29
    pub static ref TEMPLATES: Environment<'static> = {
30
        let mut env = Environment::new();
31
        macro_rules! add {
32
            (function $($id:ident),*$(,)?) => {
33
                $(env.add_function(stringify!($id), $id);)*
34
            };
35
            (filter $($id:ident),*$(,)?) => {
36
                $(env.add_filter(stringify!($id), $id);)*
37
            }
38
        }
39
        add!(function calendarize,
40
            login_path,
41
            logout_path,
42
            settings_path,
43
            help_path,
44
            list_path,
45
            list_settings_path,
46
            list_edit_path,
47
            list_post_path
48
        );
49
        add!(filter pluralize);
50
        #[cfg(not(feature = "zstd"))]
51
        #[cfg(debug_assertions)]
52
        env.set_source(minijinja::Source::from_path("web/src/templates/"));
53
        #[cfg(feature = "zstd")]
54
        #[cfg(debug_assertions)]
55
        env.set_source(minijinja::Source::from_path("web/src/templates/"));
56
        #[cfg(not(feature = "zstd"))]
57
        #[cfg(not(debug_assertions))]
58
        env.set_source(minijinja::Source::from_path("web/src/templates/"));
59
        #[cfg(feature = "zstd")]
60
        #[cfg(not(debug_assertions))]
61
        {
62
            // Load compressed templates. They are constructed in build.rs. See
63
            // [ref:embed_templates]
64
            let mut source = minijinja::Source::new();
65
            for (name, bytes) in compressed::COMPRESSED {
66
                let mut de_bytes = vec![];
67
                zstd::stream::copy_decode(*bytes,&mut de_bytes).unwrap();
68
                source.add_template(*name, String::from_utf8(de_bytes).unwrap()).unwrap();
69
            }
70
            env.set_source(source);
71
        }
72

            
73
        env
74
    };
75
}
76

            
77
pub trait StripCarets {
78
    fn strip_carets(&self) -> &str;
79
}
80

            
81
impl StripCarets for &str {
82
    fn strip_carets(&self) -> &str {
83
        let mut self_ref = self.trim();
84
        if self_ref.starts_with('<') && self_ref.ends_with('>') {
85
            self_ref = &self_ref[1..self_ref.len().saturating_sub(1)];
86
        }
87
        self_ref
88
    }
89
}
90

            
91
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize)]
92
pub struct MailingList {
93
    pub pk: i64,
94
    pub name: String,
95
    pub id: String,
96
    pub address: String,
97
    pub description: Option<String>,
98
    #[serde(serialize_with = "super::utils::to_safe_string_opt")]
99
    pub archive_url: Option<String>,
100
    pub inner: DbVal<mailpot::models::MailingList>,
101
}
102

            
103
impl From<DbVal<mailpot::models::MailingList>> for MailingList {
104
    fn from(val: DbVal<mailpot::models::MailingList>) -> Self {
105
        let DbVal(
106
            mailpot::models::MailingList {
107
                pk,
108
                name,
109
                id,
110
                address,
111
                description,
112
                archive_url,
113
            },
114
            _,
115
        ) = val.clone();
116

            
117
        Self {
118
            pk,
119
            name,
120
            id,
121
            address,
122
            description,
123
            archive_url,
124
            inner: val,
125
        }
126
    }
127
}
128

            
129
impl std::fmt::Display for MailingList {
130
    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
131
        self.id.fmt(fmt)
132
    }
133
}
134

            
135
impl Object for MailingList {
136
    fn kind(&self) -> minijinja::value::ObjectKind {
137
        minijinja::value::ObjectKind::Struct(self)
138
    }
139

            
140
    fn call_method(
141
        &self,
142
        _state: &minijinja::State,
143
        name: &str,
144
        _args: &[Value],
145
    ) -> std::result::Result<Value, Error> {
146
        match name {
147
            "subscription_mailto" => {
148
                Ok(Value::from_serializable(&self.inner.subscription_mailto()))
149
            }
150
            "unsubscription_mailto" => Ok(Value::from_serializable(
151
                &self.inner.unsubscription_mailto(),
152
            )),
153
            _ => Err(Error::new(
154
                minijinja::ErrorKind::UnknownMethod,
155
                format!("object has no method named {name}"),
156
            )),
157
        }
158
    }
159
}
160

            
161
impl minijinja::value::StructObject for MailingList {
162
    fn get_field(&self, name: &str) -> Option<Value> {
163
        match name {
164
            "pk" => Some(Value::from_serializable(&self.pk)),
165
            "name" => Some(Value::from_serializable(&self.name)),
166
            "id" => Some(Value::from_serializable(&self.id)),
167
            "address" => Some(Value::from_serializable(&self.address)),
168
            "description" => Some(Value::from_serializable(&self.description)),
169
            "archive_url" => Some(Value::from_serializable(&self.archive_url)),
170
            _ => None,
171
        }
172
    }
173

            
174
    fn static_fields(&self) -> Option<&'static [&'static str]> {
175
        Some(&["pk", "name", "id", "address", "description", "archive_url"][..])
176
    }
177
}
178

            
179
pub fn calendarize(
180
    _state: &minijinja::State,
181
    args: Value,
182
    hists: Value,
183
) -> std::result::Result<Value, Error> {
184
    use chrono::Month;
185

            
186
    macro_rules! month {
187
        ($int:expr) => {{
188
            let int = $int;
189
            match int {
190
                1 => Month::January.name(),
191
                2 => Month::February.name(),
192
                3 => Month::March.name(),
193
                4 => Month::April.name(),
194
                5 => Month::May.name(),
195
                6 => Month::June.name(),
196
                7 => Month::July.name(),
197
                8 => Month::August.name(),
198
                9 => Month::September.name(),
199
                10 => Month::October.name(),
200
                11 => Month::November.name(),
201
                12 => Month::December.name(),
202
                _ => unreachable!(),
203
            }
204
        }};
205
    }
206
    let month = args.as_str().unwrap();
207
    let hist = hists
208
        .get_item(&Value::from(month))?
209
        .as_seq()
210
        .unwrap()
211
        .iter()
212
        .map(|v| usize::try_from(v).unwrap())
213
        .collect::<Vec<usize>>();
214
    let sum: usize = hists
215
        .get_item(&Value::from(month))?
216
        .as_seq()
217
        .unwrap()
218
        .iter()
219
        .map(|v| usize::try_from(v).unwrap())
220
        .sum();
221
    let date = chrono::NaiveDate::parse_from_str(&format!("{}-01", month), "%F").unwrap();
222
    // Week = [Mon, Tue, Wed, Thu, Fri, Sat, Sun]
223
    Ok(minijinja::context! {
224
        month_name => month!(date.month()),
225
        month => month,
226
        month_int => date.month() as usize,
227
        year => date.year(),
228
        weeks => cal::calendarize_with_offset(date, 1),
229
        hist => hist,
230
        sum => sum,
231
    })
232
}
233

            
234
/// `pluralize` filter for [`minijinja`].
235
///
236
/// Returns a plural suffix if the value is not `1`, `"1"`, or an object of
237
/// length `1`. By default, the plural suffix is 's' and the singular suffix is
238
/// empty (''). You can specify a singular suffix as the first argument (or
239
/// `None`, for the default). You can specify a plural suffix as the second
240
/// argument (or `None`, for the default).
241
///
242
/// See the examples for the correct usage.
243
///
244
/// # Examples
245
///
246
/// ```rust
247
/// # use mailpot_web::pluralize;
248
/// # use minijinja::Environment;
249
///
250
/// let mut env = Environment::new();
251
/// env.add_filter("pluralize", pluralize);
252
/// for (num, s) in [
253
///     (0, "You have 0 messages."),
254
///     (1, "You have 1 message."),
255
///     (10, "You have 10 messages."),
256
/// ] {
257
///     assert_eq!(
258
///         &env.render_str(
259
///             "You have {{ num_messages }} message{{ num_messages|pluralize }}.",
260
///             minijinja::context! {
261
///                 num_messages => num,
262
///             }
263
///         )
264
///         .unwrap(),
265
///         s
266
///     );
267
/// }
268
///
269
/// for (num, s) in [
270
///     (0, "You have 0 walruses."),
271
///     (1, "You have 1 walrus."),
272
///     (10, "You have 10 walruses."),
273
/// ] {
274
///     assert_eq!(
275
///         &env.render_str(
276
///             r#"You have {{ num_walruses }} walrus{{ num_walruses|pluralize(None, "es") }}."#,
277
///             minijinja::context! {
278
///                 num_walruses => num,
279
///             }
280
///         )
281
///         .unwrap(),
282
///         s
283
///     );
284
/// }
285
///
286
/// for (num, s) in [
287
///     (0, "You have 0 cherries."),
288
///     (1, "You have 1 cherry."),
289
///     (10, "You have 10 cherries."),
290
/// ] {
291
///     assert_eq!(
292
///         &env.render_str(
293
///             r#"You have {{ num_cherries }} cherr{{ num_cherries|pluralize("y", "ies") }}."#,
294
///             minijinja::context! {
295
///                 num_cherries => num,
296
///             }
297
///         )
298
///         .unwrap(),
299
///         s
300
///     );
301
/// }
302
///
303
/// assert_eq!(
304
///     &env.render_str(
305
///         r#"You have {{ num_cherries|length }} cherr{{ num_cherries|pluralize("y", "ies") }}."#,
306
///         minijinja::context! {
307
///             num_cherries => vec![(); 5],
308
///         }
309
///     )
310
///     .unwrap(),
311
///     "You have 5 cherries."
312
/// );
313
///
314
/// assert_eq!(
315
///     &env.render_str(
316
///         r#"You have {{ num_cherries }} cherr{{ num_cherries|pluralize("y", "ies") }}."#,
317
///         minijinja::context! {
318
///             num_cherries => "5",
319
///         }
320
///     )
321
///     .unwrap(),
322
///     "You have 5 cherries."
323
/// );
324
/// assert_eq!(
325
///     &env.render_str(
326
///         r#"You have 1 cherr{{ num_cherries|pluralize("y", "ies") }}."#,
327
///         minijinja::context! {
328
///             num_cherries => true,
329
///         }
330
///     )
331
///     .unwrap()
332
///     .to_string(),
333
///     "You have 1 cherry.",
334
/// );
335
/// assert_eq!(
336
///     &env.render_str(
337
///         r#"You have {{ num_cherries }} cherr{{ num_cherries|pluralize("y", "ies") }}."#,
338
///         minijinja::context! {
339
///             num_cherries => 0.5f32,
340
///         }
341
///     )
342
///     .unwrap_err()
343
///     .to_string(),
344
///     "invalid operation: Pluralize argument is not an integer, or a sequence / object with a \
345
///      length but of type number (in <string>:1)",
346
/// );
347
/// ```
348
13
pub fn pluralize(
349
    v: Value,
350
    singular: Option<String>,
351
    plural: Option<String>,
352
) -> Result<Value, minijinja::Error> {
353
    macro_rules! int_try_from {
354
         ($ty:ty) => {
355
10
             <$ty>::try_from(v.clone()).ok().map(|v| v != 1)
356
         };
357
         ($fty:ty, $($ty:ty),*) => {
358
10
             int_try_from!($fty).or_else(|| int_try_from!($($ty),*))
359
         }
360
     }
361
52
    let is_plural: bool = v
362
        .as_str()
363
1
        .and_then(|s| s.parse::<i128>().ok())
364
1
        .map(|l| l != 1)
365
26
        .or_else(|| v.len().map(|l| l != 1))
366
24
        .or_else(|| int_try_from!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, usize))
367
14
        .ok_or_else(|| {
368
1
            minijinja::Error::new(
369
1
                minijinja::ErrorKind::InvalidOperation,
370
1
                format!(
371
                    "Pluralize argument is not an integer, or a sequence / object with a length \
372
                     but of type {}",
373
1
                    v.kind()
374
                ),
375
            )
376
2
        })?;
377
24
    Ok(match (is_plural, singular, plural) {
378
2
        (false, None, _) => "".into(),
379
2
        (false, Some(suffix), _) => suffix.into(),
380
2
        (true, _, None) => "s".into(),
381
6
        (true, _, Some(suffix)) => suffix.into(),
382
    })
383
13
}