index : sparkle-git.git

ascending towards madness

use std::{
    path::{Path, PathBuf},
    vec,
};

use comrak::Options;
use serde::{Deserialize, Serialize};
use tracing::warn;

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct AppConfig {
    /// Set the title and heading of the repository index page
    ///
    /// Default value: "Git repository browser"
    pub root_title: String,

    /// Set a subtitle for the repository index page
    ///
    /// Default value: `unset`
    pub root_description: String,

    /// Include some more info about example.com on the index page
    ///
    /// Default value: `unset`
    pub root_readme: String,

    /// Allow download of source tarballs
    ///
    /// Default value: `["tar.bz", "tar.bz2", "zip"]`
    pub snapshots: Vec<String>,

    /// Add a favicon
    ///
    /// Default value: `unset`
    pub favicon: String,

    /// Use a custom logo image
    ///
    /// Default value: `unset`
    pub logo: String,

    /// Alt text for the custom logo image
    ///
    /// Used as a fallback when `logo` is unset
    ///
    /// Default value: "🏡"
    pub logo_alt: String,

    /// Url loaded when clicking on the logo
    ///
    /// Default value: "/"
    pub logo_link: String,

    /// Allow http transport git clone
    ///
    /// Default value: `true`
    pub enable_http_clone: bool,

    /// Show extra links for each repository on the index page
    ///
    /// Default value: `false`
    pub enable_index_links: bool,

    /// Show owner on the index page
    ///
    /// Default value: `true`
    pub enable_index_owner: bool,

    /// Generate HTTPS clone urls
    ///
    /// Default value: `unset`
    pub clone_prefix: String,

    /// Generate SSH clone urls
    ///
    /// Default value: `unset`
    pub ssh_clone_prefix: String,

    /// Specifies the maximum number of commit message characters to display in "log" view.
    ///
    /// A value of "0" indicates no limit.
    ///
    /// Default value: "0"
    pub max_commit_message_length: usize,

    /// Specifies the maximum number of description characters to display on the repository index page.
    ///
    /// A value of "0" indicates no limit.
    ///
    /// Default value: "0"
    pub max_repo_desc_length: usize,

    /// Specifies an absolute path on the filesystem to the css document to include in all pages.
    ///
    /// If set, the default stylesheet will be replaced with this one.
    ///
    /// Default value: `None`
    pub css: Option<String>,

    /// Specifies an absolute path on the filesystem to the file to serve at `/robots.txt`.
    ///
    /// If set, the default `robots.txt` will be replaced with this one.
    ///
    /// Default value: `None`
    pub robots_txt: Option<String>,

    /// Specifies the style of header to use.
    ///
    /// `Default` is the default style of an emoji and title text
    ///
    /// `Text` is only the title text and a subtitle (if applicable)
    ///
    /// `Image` is an image, title text, and a subtitle (if applicable)
    ///
    /// Default value: `Default`
    pub header: Option<HeaderStyle>,
}

impl ::std::default::Default for AppConfig {
    fn default() -> Self {
        Self {
            root_title: String::from("Git repository browser"),
            root_description: String::new(),
            root_readme: String::new(),
            snapshots: vec![
                String::from("tar.gz"),
                String::from("tar.bz2"),
                String::from("zip"),
            ],
            favicon: String::new(),
            logo: String::from("🏡"),
            logo_alt: String::new(),
            logo_link: String::from("/"),
            enable_http_clone: true,
            enable_index_links: false,
            enable_index_owner: true,
            clone_prefix: String::new(),
            ssh_clone_prefix: String::new(),
            max_commit_message_length: 0,
            max_repo_desc_length: 0,
            css: None,
            robots_txt: None,
            header: Some(HeaderStyle::default()),
        }
    }
}

impl AppConfig {
    pub fn load(path: String) -> Self {
        confy::load_path(path).unwrap_or_default()
    }

    pub fn http_clone_enabled(&self) -> bool {
        (!self.clone_prefix.is_empty() || !self.ssh_clone_prefix.is_empty())
            && self.enable_http_clone
    }

    pub fn root_readme_is_valid(&self) -> bool {
        Path::new(&self.root_readme).try_exists().unwrap_or(false)
    }

    /// Loads a (s)css file from disk and compiles it using `rsass`
    ///
    /// if `Ok()`, returns the file contents.
    /// If `Err()` or `None`, returns an empty slice.
    pub fn get_css_data(&self) -> Box<[u8]> {
        self.css
            .as_ref()
            .map(|file| {
                let format = rsass::output::Format {
                    style: rsass::output::Style::Compressed,
                    ..rsass::output::Format::default()
                };

                rsass::compile_scss_path(&PathBuf::from(file), format).unwrap_or_else(|_| {
                    warn!(
                        "Unable to load or build css from path {:?}. Using defaults.",
                        file
                    );
                    Vec::new()
                })
            })
            .unwrap_or_else(|| Vec::new())
            .into_boxed_slice()
    }
}

pub struct ReadmeConfig;

impl ReadmeConfig {
    pub fn gfm() -> Options {
        // enable gfm extensions
        // https://github.github.com/gfm/
        let mut options = Options::default();
        options.extension.autolink = true;
        options.extension.footnotes = true;
        options.extension.strikethrough = true;
        options.extension.table = true;
        options.extension.tagfilter = true;
        options.extension.tasklist = true;

        options
    }
}

/// Represents available header styles for rendering the frontend.
#[derive(Debug, Default, Clone, Serialize, PartialEq, Eq)]
pub enum HeaderStyle {
    /// Emoji logo, text title
    #[default]
    Default,
    /// Text title, text subtitle
    Text,
    /// Image logo, text title, text subtitle
    Image,
}

impl<'de> Deserialize<'de> for HeaderStyle {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let variant: &str = Deserialize::deserialize(deserializer)?;
        // Support case insensitivity
        match variant.to_lowercase().as_str() {
            "default" => Ok(HeaderStyle::Default),
            "text" => Ok(HeaderStyle::Text),
            "image" => Ok(HeaderStyle::Image),
            _ => Ok(HeaderStyle::Default),
        }
    }
}