scraps 5.0.0

Scraps is a portable CLI knowledge hub for managing interconnected Markdown documentation with Wiki-link notation.
use super::renderer::BuildRenderer;
use crate::error::ScrapsResult;
use crate::usecase::progress::{Progress, Stage};
use rayon::iter::IntoParallelIterator;
use rayon::prelude::*;
use scraps_libs::{
    html::{self, EmbedMode, LinkTargets},
    model::{scrap::Scrap, tags::Tags},
};
use tracing::{Level, span};

use super::model::{
    backlinks_map::BacklinksMap,
    build_config::BuildConfig,
    scrap_detail::{ScrapDetail, ScrapDetails},
    scrap_types::ScrapTypes,
    site_nav::SiteNav,
};
use std::collections::HashMap;

pub struct BuildUsecase;

impl BuildUsecase {
    pub fn new() -> BuildUsecase {
        BuildUsecase
    }

    pub fn execute<PG: Progress, BR: BuildRenderer>(
        &self,
        scraps_with_ts: &[(Scrap, Option<i64>)],
        readme_text: &Option<String>,
        progress: &PG,
        renderer: &BR,
        config: &BuildConfig,
    ) -> ScrapsResult<usize> {
        let BuildConfig {
            base_url,
            timezone,
            html_metadata,
            css_metadata,
            list_view_configs,
        } = config;
        let timezone = *timezone;

        progress.start_stage(&Stage::ReadScraps);
        let span_read_scraps = span!(Level::INFO, "read_scraps").entered();

        let link_targets = LinkTargets::new(scraps_with_ts.iter().map(|(scrap, _)| scrap));

        // Process README content
        let readme_content = readme_text
            .as_ref()
            .map(|text| html::to_content(text, &None, base_url, EmbedMode::Expand(&link_targets)));

        // Build ScrapDetails from pre-loaded data
        let scrap_details = scraps_with_ts
            .into_par_iter()
            .map(|(scrap, commited_ts)| {
                ScrapDetail::new(scrap, commited_ts, base_url, &link_targets)
            })
            .collect::<Vec<ScrapDetail>>();
        let scrap_details = ScrapDetails::new(scrap_details);

        let scraps = scrap_details.to_scraps();
        let backlinks_map = BacklinksMap::new(&scraps);
        let scraps_by_key: HashMap<_, _> = scraps
            .iter()
            .map(|scrap| (scrap.self_key(), scrap))
            .collect();
        let site_nav = SiteNav::new(
            scraps.len(),
            Tags::new(&scraps),
            ScrapTypes::new(&scraps),
            list_view_configs.build_search_index,
            timezone,
            readme_content.is_some(),
        );
        span_read_scraps.exit();
        progress.complete_stage(&Stage::ReadScraps, &scrap_details.len());

        // generate html
        progress.start_stage(&Stage::GenerateHtml);

        // generate html index
        let span_generate_html_indexes = span!(Level::INFO, "generate_html_indexes").entered();
        let index_page_count = renderer.render_index(
            base_url,
            html_metadata,
            list_view_configs,
            &scrap_details,
            &backlinks_map,
            &site_nav,
        )?;
        span_generate_html_indexes.exit();

        // generate html about page from README, when present
        if let Some(readme) = &readme_content {
            let span_generate_html_about = span!(Level::INFO, "generate_html_about").entered();
            renderer.render_about(base_url, html_metadata, readme, &backlinks_map, &site_nav)?;
            span_generate_html_about.exit();
        }

        // generate html scraps
        let span_generate_html_scraps = span!(Level::INFO, "generate_html_scraps").entered();
        renderer.render_scraps(
            base_url,
            html_metadata,
            &scrap_details,
            &backlinks_map,
            &scraps_by_key,
            &site_nav,
        )?;
        span_generate_html_scraps.exit();

        // generate html scraps index
        let span_generate_html_scraps_index =
            span!(Level::INFO, "generate_html_scraps_index").entered();
        renderer.render_scraps_index(
            base_url,
            html_metadata,
            &scrap_details,
            &backlinks_map,
            &site_nav,
        )?;
        span_generate_html_scraps_index.exit();

        // generate html tags index
        let span_generate_html_tags_index =
            span!(Level::INFO, "generate_html_tags_index").entered();
        renderer.render_tags_index(base_url, html_metadata, &scraps, &backlinks_map, &site_nav)?;
        span_generate_html_tags_index.exit();

        // generate html tags
        let span_generate_html_tags = span!(Level::INFO, "generate_html_tags").entered();
        renderer.render_tags(base_url, html_metadata, &backlinks_map, &site_nav)?;
        span_generate_html_tags.exit();

        progress.complete_stage(&Stage::GenerateHtml, &{
            index_page_count + scrap_details.len() + 1 + // tags index
                site_nav.tags.len() + site_nav.types.len() // all scraps of each type
        });

        // generate css
        progress.start_stage(&Stage::GenerateCss);
        let span_generate_css = span!(Level::INFO, "generate_css").entered();
        renderer.render_css(css_metadata)?;
        span_generate_css.exit();
        progress.complete_stage(&Stage::GenerateCss, &1);

        // generate fonts
        let span_generate_fonts = span!(Level::INFO, "generate_fonts").entered();
        renderer.render_fonts()?;
        span_generate_fonts.exit();

        // generate js
        let span_generate_js = span!(Level::INFO, "generate_js").entered();
        renderer.render_js()?;
        span_generate_js.exit();

        // generate search index json when build_search_index is true
        if list_view_configs.build_search_index {
            progress.start_stage(&Stage::GenerateJson);
            let _span_generate_json_search_index =
                span!(Level::INFO, "generate_json_search_index").entered();
            renderer.render_search_index(base_url, &scraps)?;
            progress.complete_stage(&Stage::GenerateJson, &1);
        }

        Ok(scraps.len())
    }
}

#[cfg(test)]
mod tests {
    use crate::usecase::build::model::{
        color_scheme::ColorScheme, css::CssMetadata, html::HtmlMetadata,
        list_view_configs::ListViewConfigs, paging::Paging,
    };
    use crate::usecase::build::renderer::tests::BuildRendererTest;
    use crate::usecase::progress::tests::ProgressTest;

    use super::*;
    use scraps_libs::lang::LangCode;
    use scraps_libs::model::base_url::BaseUrl;
    use url::Url;

    fn build_config(build_search_index: bool) -> BuildConfig {
        BuildConfig {
            base_url: BaseUrl::new(Url::parse("http://localhost:1112/").unwrap()).unwrap(),
            timezone: chrono_tz::UTC,
            html_metadata: HtmlMetadata::new(
                &LangCode::default(),
                "Scrap",
                &Some("Scrap Wiki".to_string()),
                &Some(Url::parse("https://github.io/image.png").unwrap()),
            ),
            css_metadata: CssMetadata::new(&ColorScheme::OsSetting),
            list_view_configs: ListViewConfigs::new(&build_search_index, &Paging::Not),
        }
    }

    #[test]
    fn it_run() {
        let scraps_with_ts = vec![
            (
                Scrap::new("test1", &None, concat!("# header1\n", "## header2\n")),
                Some(0i64),
            ),
            (Scrap::new("test2", &None, "[[test1]]\n"), Some(0i64)),
        ];

        let readme_text = Some("# README\n".to_string());

        let progress = ProgressTest::new();
        let renderer = BuildRendererTest::new();

        let usecase = BuildUsecase::new();
        let result = usecase
            .execute(
                &scraps_with_ts,
                &readme_text,
                &progress,
                &renderer,
                &build_config(true),
            )
            .unwrap();
        assert_eq!(result, 2);
    }

    #[test]
    fn it_run_when_build_search_index_is_false() {
        let scraps_with_ts = vec![
            (
                Scrap::new("test1", &None, concat!("# header1\n", "## header2\n")),
                Some(0i64),
            ),
            (Scrap::new("test2", &None, "[[test1]]\n"), Some(0i64)),
        ];

        let progress = ProgressTest::new();
        let renderer = BuildRendererTest::new();

        let usecase = BuildUsecase::new();
        let result = usecase
            .execute(
                &scraps_with_ts,
                &None,
                &progress,
                &renderer,
                &build_config(false),
            )
            .unwrap();
        assert_eq!(result, 2);
    }
}