scraps 2.3.1

Scraps is a portable CLI knowledge hub for managing interconnected Markdown documentation with Wiki-link notation.
use std::path::{Path, PathBuf};

use crate::error::ScrapsResult;
use crate::service::tera_render::{render_to_file, resolve_template, user_template_glob};
use crate::usecase::build::html::templates;
use crate::usecase::build::model::backlinks_map::BacklinksMap;
use crate::usecase::build::model::html::HtmlMetadata;
use crate::usecase::build::model::scrap_detail::ScrapDetails;
use crate::usecase::build::model::site_nav::SiteNav;
use scraps_libs::model::base_url::BaseUrl;
use tera::Tera;

use super::serde::title_index::TitleIndexTera;

/// The paginated index answers "what changed lately"; this one answers "what
/// is in here": every scrap on a single page, grouped by the title's initial
/// with a jump bar, so a reader who knows the name goes straight to it.
pub struct ScrapsIndexRender {
    tera: Tera,
    output_scraps_dir_path: PathBuf,
}

impl ScrapsIndexRender {
    pub fn new(static_dir_path: &Path, output_dir_path: &Path) -> ScrapsResult<ScrapsIndexRender> {
        let tera = templates::scraps_index(&user_template_glob(static_dir_path, "*.html"))?;

        Ok(ScrapsIndexRender {
            tera,
            output_scraps_dir_path: output_dir_path.join("scraps"),
        })
    }

    pub fn run(
        &self,
        base_url: &BaseUrl,
        metadata: &HtmlMetadata,
        scrap_details: &ScrapDetails,
        backlinks_map: &BacklinksMap,
        site_nav: &SiteNav,
    ) -> ScrapsResult<()> {
        let mut context = templates::context(base_url, metadata);
        templates::insert_site_nav(&mut context, "scraps", site_nav, backlinks_map);
        context.insert("groups", &TitleIndexTera::new(scrap_details, backlinks_map));

        let template_name = resolve_template(
            &self.tera,
            "scraps_index.html",
            "__builtins/scraps_index.html",
        );
        let file_path = self.output_scraps_dir_path.join("index.html");
        render_to_file(&self.tera, template_name, &context, &file_path)
    }
}

#[cfg(test)]
mod tests {
    use crate::test_fixtures::{temp_scrap_project, TempScrapProject};
    use crate::usecase::build::model::scrap_detail::ScrapDetail;
    use rstest::rstest;
    use scraps_libs::{
        lang::LangCode, model::base_url::BaseUrl, model::scrap::Scrap, model::tags::Tags,
    };
    use std::fs;
    use url::Url;

    use super::*;

    #[rstest]
    fn it_run(#[from(temp_scrap_project)] project: TempScrapProject) {
        project.add_static_file(
            "scraps_index.html",
            b"{% for group in groups %}[{{ group.label }}{% for scrap in group.scraps %} {{ scrap.title }}:{{ scrap.backlinks_count }}{% endfor %}]{% endfor %}",
        );

        let base_url = BaseUrl::new(Url::parse("http://localhost:1112/").unwrap()).unwrap();
        let metadata = HtmlMetadata::new(&LangCode::default(), "Scrap", &None, &None);

        // Mixed initials and mixed case, to pin grouping and in-group order.
        let scrap1 = Scrap::new("beta", &None, "[[Alpha]]");
        let scrap2 = Scrap::new("Alpha", &None, "");
        let scrap3 = Scrap::new("デザイントークン", &None, "");
        let scraps = [scrap1.clone(), scrap2.clone(), scrap3.clone()];
        let scrap_texts = scraps
            .iter()
            .map(|s| (s.self_key(), s.md_text().to_string()))
            .collect();

        let details = ScrapDetails::new(&vec![
            ScrapDetail::new(&scrap1, &None, &base_url, &scrap_texts),
            ScrapDetail::new(&scrap2, &None, &base_url, &scrap_texts),
            ScrapDetail::new(&scrap3, &None, &base_url, &scrap_texts),
        ]);
        let backlinks_map = BacklinksMap::new(&scraps);
        let site_nav = SiteNav::new(
            scraps.len(),
            Tags::new(&scraps),
            true,
            chrono_tz::UTC,
            false,
        );

        let render = ScrapsIndexRender::new(&project.static_dir, &project.output_dir).unwrap();
        render
            .run(&base_url, &metadata, &details, &backlinks_map, &site_nav)
            .unwrap();

        let result = fs::read_to_string(project.output_path("scraps/index.html")).unwrap();
        assert_eq!(result, "[た デザイントークン:0][A Alpha:1][B beta:0]");
    }

    #[rstest]
    fn builtin_template_renders_jump_bar(#[from(temp_scrap_project)] project: TempScrapProject) {
        let base_url = BaseUrl::new(Url::parse("http://localhost:1112/").unwrap()).unwrap();
        let metadata = HtmlMetadata::new(&LangCode::default(), "Scrap", &None, &None);

        let scrap1 = Scrap::new("デザイントークン", &None, "");
        let scraps = [scrap1.clone()];
        let scrap_texts = scraps
            .iter()
            .map(|s| (s.self_key(), s.md_text().to_string()))
            .collect();

        let details = ScrapDetails::new(&vec![ScrapDetail::new(
            &scrap1,
            &None,
            &base_url,
            &scrap_texts,
        )]);
        let backlinks_map = BacklinksMap::new(&scraps);
        let site_nav = SiteNav::new(
            scraps.len(),
            Tags::new(&scraps),
            true,
            chrono_tz::UTC,
            false,
        );

        let render = ScrapsIndexRender::new(&project.static_dir, &project.output_dir).unwrap();
        render
            .run(&base_url, &metadata, &details, &backlinks_map, &site_nav)
            .unwrap();

        let result = fs::read_to_string(project.output_path("scraps/index.html")).unwrap();
        assert!(result.contains("class=\"jump\""));
        assert!(result.contains("デザイントークン"));
    }
}