summary history branches tags files
src/template.rs
/*
 * Copyright 2023 Trevor Bentley
 *
 * Author: Trevor Bentley
 * Contact: gitsy@@trevorbentley.com
 * Source: https://github.com/mrmekon/itsy-gitsy
 *
 * This file is part of Itsy-Gitsy.
 *
 * Itsy-Gitsy is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Itsy-Gitsy 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Itsy-Gitsy.  If not, see <http://www.gnu.org/licenses/>.
 */
use crate::git::GitFile;
use crate::util::{sanitize_path_component, urlify_path};
use chrono::{naive::NaiveDateTime, offset::FixedOffset, DateTime};
use serde::Serialize;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use tera::{from_value, to_value, try_get_value, Filter, Function, Value};

fn ts_to_date(ts: i64, offset: Option<i64>, format: Option<String>) -> String {
    let offset = offset.unwrap_or(0);
    let dt = NaiveDateTime::from_timestamp_opt(ts + offset, 0).expect("Invalid timestamp");
    let dt_tz: DateTime<FixedOffset> =
        DateTime::from_local(dt, FixedOffset::east_opt(offset as i32).expect("Invalid timezone"));
    match format {
        Some(f) => dt_tz.format(&f).to_string(),
        None => dt_tz.format("%Y-%m-%d").to_string(),
    }
}

fn ts_to_git_timestamp(ts: i64, offset: Option<i64>) -> String {
    let offset = offset.unwrap_or(0);
    let dt = chrono::naive::NaiveDateTime::from_timestamp_opt(ts + offset, 0).expect("invalid timestamp");
    let dt_tz: DateTime<FixedOffset> =
        DateTime::from_local(dt, FixedOffset::east_opt(offset as i32).expect("Invalid timezone"));
    dt_tz.format("%a %b %e %T %Y %z").to_string()
}

pub struct FileFilter;
impl Filter for FileFilter {
    fn filter(&self, value: &Value, _args: &HashMap<String, Value>) -> Result<Value, tera::Error> {
        let file_list: Vec<GitFile> = try_get_value!("only_files", "value", Vec<GitFile>, value);
        let file_list: Vec<GitFile> = file_list
            .iter()
            .filter_map(|x| match x.kind.as_str() {
                "file" => Some(x.clone()),
                _ => None,
            })
            .collect();
        Ok(to_value(file_list).unwrap())
    }
}

pub struct DirFilter;
impl Filter for DirFilter {
    fn filter(&self, value: &Value, _args: &HashMap<String, Value>) -> Result<Value, tera::Error> {
        let file_list: Vec<GitFile> = try_get_value!("only_dirs", "value", Vec<GitFile>, value);
        let file_list: Vec<GitFile> = file_list
            .iter()
            .filter_map(|x| match x.kind.as_str() {
                "dir" => Some(x.clone()),
                _ => None,
            })
            .collect();
        Ok(to_value(file_list).unwrap())
    }
}

pub struct HexFilter;
impl Filter for HexFilter {
    fn filter(&self, value: &Value, _args: &HashMap<String, Value>) -> Result<Value, tera::Error> {
        let v: i64 = try_get_value!("hex", "value", i64, value);
        Ok(to_value(format!("{:x}", v)).unwrap())
    }
}

pub struct OctFilter;
impl Filter for OctFilter {
    fn filter(&self, value: &Value, _args: &HashMap<String, Value>) -> Result<Value, tera::Error> {
        let v: i64 = try_get_value!("oct", "value", i64, value);
        Ok(to_value(format!("{:o}", v)).unwrap())
    }
}

pub struct MaskFilter;
impl Filter for MaskFilter {
    fn filter(&self, value: &Value, args: &HashMap<String, Value>) -> Result<Value, tera::Error> {
        let v: u64 = try_get_value!("mask", "value", u64, value);
        let mask: String = from_value(
            args.get("mask")
                .expect("ERROR: Tera mask filter called without `mask` parameter.")
                .clone(),
        )
        .expect("ERROR: Tera `mask` parameter is not valid.");
        let mask: u64 = match mask.starts_with("0x") {
            true => {
                let hexstr = mask.strip_prefix("0x").unwrap();
                u64::from_str_radix(hexstr, 16).expect("ERROR: Tera `mask` parameter is invalid hex.")
            }
            false => str::parse::<u64>(&mask).expect("ERROR: Tera `mask` parameter is not valid."),
        };
        Ok(to_value(v & mask).unwrap())
    }
}

pub struct UrlStringFilter;
impl Filter for UrlStringFilter {
    fn filter(&self, value: &Value, _args: &HashMap<String, Value>) -> Result<Value, tera::Error> {
        let v: String = try_get_value!("url_string", "value", String, value);
        let sanitized = sanitize_path_component(&urlify_path(&v));
        Ok(to_value(sanitize_path_component(&sanitized)).unwrap())
    }
}

pub struct TsDateFn;
impl Function for TsDateFn {
    fn call(&self, args: &HashMap<String, Value>) -> Result<Value, tera::Error> {
        let ts: Option<i64> = match args.get("ts") {
            Some(ts) => match tera::from_value(ts.clone()) {
                Ok(ts) => Some(ts),
                _ => None,
            },
            _ => None,
        };
        let ts = ts.expect("ts_to_date missing a `ts` argument");

        let tz: Option<i64> = match args.get("tz") {
            Some(tz) => match tera::from_value(tz.clone()) {
                Ok(tz) => Some(tz),
                _ => None,
            },
            _ => None,
        };

        let fmt: Option<String> = match args.get("fmt") {
            Some(fmt) => match tera::from_value(fmt.clone()) {
                Ok(fmt) => Some(fmt),
                _ => None,
            },
            _ => None,
        };
        Ok(to_value(ts_to_date(ts, tz, fmt)).unwrap())
    }
}

pub struct TsTimestampFn;
impl Function for TsTimestampFn {
    fn call(&self, args: &HashMap<String, Value>) -> Result<Value, tera::Error> {
        let ts: Option<i64> = match args.get("ts") {
            Some(ts) => match tera::from_value(ts.clone()) {
                Ok(ts) => Some(ts),
                _ => None,
            },
            _ => None,
        };
        let ts = ts.expect("ts_to_git_timestamp missing a `ts` argument");

        let tz: Option<i64> = match args.get("tz") {
            Some(tz) => match tera::from_value(tz.clone()) {
                Ok(tz) => Some(tz),
                _ => None,
            },
            _ => None,
        };
        Ok(to_value(ts_to_git_timestamp(ts, tz)).unwrap())
    }
}

#[derive(Serialize)]
pub struct Pagination {
    pub pages: usize,
    pub page_idx: usize,
    pub page_str: String,
    pub cur_page: String,
    pub next_page: Option<String>,
    pub prev_page: Option<String>,
}
impl Pagination {
    pub fn new<P: AsRef<Path>>(cur: usize, total: usize, url_template: &P) -> Self {
        let url_template = url_template.as_ref().to_str().expect(&format!(
            "ERROR: attempted to paginate unparseable path: {}",
            url_template.as_ref().display()
        ));
        let digits = total.to_string().len().max(2);
        let next = match cur + 1 <= total {
            true => Some(cur + 1),
            false => None,
        };
        let prev = match cur <= 1 {
            true => None,
            false => Some(cur - 1),
        };
        let cur_str = match cur <= 1 {
            true => String::new(),
            false => format!("{:0w$}", cur, w = digits),
        };
        let next_str = match next.unwrap_or(0) <= 1 {
            true => String::new(),
            false => format!("{:0w$}", next.unwrap_or(0), w = digits),
        };
        let prev_str = match prev.unwrap_or(0) <= 1 {
            true => String::new(),
            false => format!("{:0w$}", prev.unwrap_or(0), w = digits),
        };
        let cur_page = url_template.replace("%PAGE%", &cur_str);
        let next_page = match next {
            Some(_) => Some(url_template.replace("%PAGE%", &next_str)),
            _ => None,
        };
        let prev_page = match prev {
            Some(_) => Some(url_template.replace("%PAGE%", &prev_str)),
            _ => None,
        };
        Pagination {
            pages: total,
            page_idx: cur,
            page_str: cur_str.clone(),
            cur_page,
            next_page,
            prev_page,
        }
    }

    pub fn with_relative_paths(&self) -> Self {
        let cur_page = {
            let path = PathBuf::from(&self.cur_page);
            path.file_name()
                .expect(&format!("Invalid output filename: {}", self.cur_page))
                .to_string_lossy()
                .to_string()
        };
        let next_page = match &self.next_page {
            Some(p) => {
                let path = PathBuf::from(p);
                Some(
                    path.file_name()
                        .expect(&format!("Invalid output filename: {}", p))
                        .to_string_lossy()
                        .to_string(),
                )
            }
            _ => None,
        };
        let prev_page = match &self.prev_page {
            Some(p) => {
                let path = PathBuf::from(p);
                Some(
                    path.file_name()
                        .expect(&format!("Invalid output filename: {}", p))
                        .to_string_lossy()
                        .to_string(),
                )
            }
            _ => None,
        };
        Pagination {
            pages: self.pages,
            page_idx: self.page_idx,
            page_str: self.page_str.clone(),
            cur_page,
            next_page,
            prev_page,
        }
    }
}