marky 0.9.0

Markdown Magician 🧙
use crate::warn;
use crate::{included::VENDOR_DIR, paths};
use anyhow::Result;
use colored::Colorize;

use std::fs::File;
use std::io::{self, prelude::*};
use std::path::PathBuf;

#[derive(serde::Deserialize, Clone)]
pub struct Theme {
    pub name: String,

    path: Option<PathBuf>,
    inline: Option<String>,
}

impl Theme {
    pub fn resolve(&self) -> Result<String> {
        let css = {
            if self.inline.is_some() {
                self.resolve_inline()?
            } else if self.path.is_some() {
                self.resolve_path()?
            } else {
                return Err(anyhow::Error::new(io::Error::new(
                    std::io::ErrorKind::Other,
                    "theme source is not specified",
                )));
            }
        };

        let result = minifier::css::minify(css.as_str())
            .map(|m| m.to_string())
            .unwrap_or(css);

        Ok(result)
    }

    fn resolve_inline(&self) -> std::io::Result<String> {
        assert!(self.inline.is_some());

        let inline = self.inline.as_ref().unwrap().to_string();
        Ok(inline)
    }

    fn resolve_path(&self) -> std::io::Result<String> {
        assert!(self.path.is_some());

        let path = {
            let path = self.path.as_ref().unwrap();

            if path.is_relative() {
                paths::dirs::config().join(path)
            } else {
                path.clone()
            }
        };

        let mut file = File::open(path)?;
        let mut contents = String::new();
        file.read_to_string(&mut contents)?;

        Ok(contents)
    }
}

impl Default for Theme {
    fn default() -> Self {
        Themes::default().by_name("sakura").unwrap()
    }
}

#[derive(serde::Deserialize)]
pub struct Themes {
    pub themes: Vec<Theme>,
}

impl Themes {
    pub fn by_name(&self, name: &str) -> Option<Theme> {
        self.themes.iter().find(|theme| theme.name == name).cloned()
    }

    pub fn closest_match(&self, name: &str) -> Option<Theme> {
        use levenshtein::levenshtein;

        self.themes
            .iter()
            .min_by(|a, b| {
                levenshtein(name, a.name.as_str()).cmp(&levenshtein(name, b.name.as_str()))
            })
            .cloned()
    }
}

impl Default for Themes {
    fn default() -> Self {
        let themes: Vec<Theme> = VENDOR_DIR
            .get_dir("themes")
            .expect("themes directory in vendor/ must be present")
            .entries()
            .into_iter()
            .filter_map(|entry| entry.as_file())
            .filter_map(|file| {
                let path = file.path();

                if path.extension().map(|ext| ext != "css").unwrap_or(true) {
                    return None;
                }

                let name = path.file_stem().unwrap().to_str().unwrap().to_string();
                match std::str::from_utf8(file.contents()) {
                    Ok(contents) => Some((name, contents)),
                    Err(e) => {
                        warn!("can't parse theme {}: {}", name.cyan(), e);
                        None
                    }
                }
            })
            .map(|(name, contents)| Theme {
                name,
                inline: Some(contents.to_string()),
                path: None,
            })
            .collect();

        Themes { themes }
    }
}

pub fn available_themes() -> Result<Themes> {
    let mut default = Themes::default();

    let themes_path = paths::files::themes();
    if !themes_path.exists() {
        return Ok(default);
    }

    let mut file = File::open(themes_path)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;

    let mut custom: Themes = toml::from_str(contents.as_str())?;

    default.themes.append(&mut custom.themes);

    Ok(default)
}