oubli 0.1.2

Library for generating documentation websites with Maudit.
Documentation
use maudit::content::ContentEntry;
use maudit::page::prelude::*;

use maudit::{
    content::{ContentSourceInternal, ContentSources},
    coronate,
    page::{prelude::Params, FullPage},
};

// Re-expose Maudit's public API.
pub use maudit::{content_sources, routes, BuildOptions, BuildOutput};
// Expose the archetypes module.
pub mod archetypes {
    pub mod blog;
}
// Expose the layout module.
pub mod layouts {
    mod layout;
    pub use layout::layout;
}
// Expose public components.
pub mod components {}

/// Help you quickly scaffold common types of content, like blogs or documentation.
#[derive(Debug, Clone)]
pub enum Archetype {
    /// Represents a markdown blog archetype.
    Blog,
    /// Represents a markdown documentation archetype.
    MarkdownDoc,
}

#[macro_export]
/// Helps to define every archetype that should be build by [`forget()`].
///
/// ## Example
/// ```rs
/// use oubli::{Archetype, archetypes, content_sources, forget, routes, BuildOptions, BuildOutput};
///
/// fn main() -> Result<BuildOutput, Box<dyn std::error::Error>> {
///     forget(
///         // Define archetypes and their glob patterns using the provided macro.
///         archetypes![
///             ("News", news, Archetype::Blog, "content/blog/**/*.md")
///         ],
///         routes![],
///         content_sources![],
///         BuildOptions::default(),
///     )
/// }
/// ```
macro_rules! archetypes {
    ($(($name:expr, $ident:ident, $arch:expr, $glob:expr)),* $(,)?) => {{
        let mut vec = Vec::new();
        $(
            let tuple = match $arch {
                oubli::Archetype::Blog => {
                    // Generate the content source
                    let content_source = maudit::content::ContentSource::new(
                        stringify!($ident),
                        Box::new({
                            let glob = $glob.to_string();
                            move || maudit::content::glob_markdown::<oubli::archetypes::blog::BlogEntryContent>(&glob, None)
                        }),
                    );
                    // Generate the pages
                    mod $ident {
                        use maudit::page::prelude::*;
                        use oubli::archetypes::blog::*;

                        #[route(stringify!($ident))]
                        pub struct Index;
                        impl Page for Index {
                            fn render(&self, ctx: &mut RouteContext) -> RenderResult {
                                blog_index_content::<Entry>(Entry, ctx, $name, stringify!($ident)).into()
                            }
                        }

                        #[route(concat!(stringify!($ident), "/[entry]"))]
                        pub struct Entry;
                        impl Page<BlogEntryParams> for Entry {
                            fn render(&self, ctx: &mut RouteContext) -> RenderResult {
                                blog_entry_render(ctx, $name, stringify!($ident)).into()
                            }

                            fn routes(&self, ctx: &mut DynamicRouteContext) -> Vec<Route<BlogEntryParams>> {
                                blog_entry_routes(ctx, stringify!($ident))
                            }
                        }
                    }
                    ($name, stringify!($ident), vec![&$ident::Index as &dyn maudit::page::FullPage, &$ident::Entry as &dyn maudit::page::FullPage], Box::new(content_source) as Box<dyn maudit::content::ContentSourceInternal>)
                },
                oubli::Archetype::MarkdownDoc => {
                    todo!();
                }
            };
            vec.push(tuple);
        )*
        vec
    }};
}

/// 🪶 Oubli entrypoint. Starts the build process and generates the output files.
///
/// This function wraps Maudit's [`coronate`](maudit::coronate) function and adds an `archetypes` argument.
/// The user can specify one or more archetypes with their corresponding glob pattern. The routes and content
/// sources generated by these archetypes are merged with the routes and content sources provided directly.
///
/// ## Example
/// ```rs
/// use oubli::{Archetype, archetypes, content_sources, forget, routes, BuildOptions, BuildOutput};
///
/// fn main() -> Result<BuildOutput, Box<dyn std::error::Error>> {
///     forget(
///         // Define archetypes and their glob patterns using the provided macro.
///         archetypes![
///             ("News", news, Archetype::Blog, "content/blog/**/*.md")
///         ],
///         routes![],
///         content_sources![],
///         BuildOptions::default(),
///     )
/// }
/// ```
#[allow(clippy::type_complexity)]
pub fn forget(
    archetypes: Vec<(
        &str,
        &str,
        Vec<&dyn FullPage>,
        Box<dyn ContentSourceInternal>,
    )>,
    routes: &[&dyn FullPage],
    mut content_sources: ContentSources,
    options: BuildOptions,
) -> Result<BuildOutput, Box<dyn std::error::Error>> {
    // Let's merge the routes and content sources from the archetypes to the user-provided ones.
    let mut combined_routes = routes.to_vec();
    let mut content_sources_archetypes = Vec::new();

    content_sources
        .0
        .push(generate_archetype_store(&archetypes));

    for (_name, _stringified_ident, pages, content_source) in archetypes {
        content_sources_archetypes.push(content_source);
        combined_routes.extend(pages);
    }

    content_sources.0.extend(content_sources_archetypes);

    // At the end of the day, we are just a Maudit wrapper.
    coronate(&combined_routes, content_sources, options)
}

/// # Generates a content source with every provided archetype.
fn generate_archetype_store(archetypes: &Vec<ArchetypeTuple>) -> Box<dyn ContentSourceInternal> {
    let names: Vec<(String, String)> = archetypes
        .iter()
        .map(|(name, stringified_ident, _, _)| (name.to_string(), stringified_ident.to_string()))
        .collect();

    let archetype_store = maudit::content::ContentSource::new(
        "archetype_store",
        Box::new(move || {
            let mut entries = Vec::new();
            for (name, stringified_ident) in names.iter() {
                entries.push(ContentEntry::new(
                    stringified_ident.to_string(),
                    None,
                    None,
                    ArchetypeStoreEntry {
                        title: name.to_string(),
                    },
                    None,
                ));
            }
            entries
        }),
    );

    Box::new(archetype_store)
}

/// Represents each archetype provided by the user.
#[derive(Params)]
pub struct ArchetypeStoreEntry {
    pub title: String,
}

/// # Internal representation of an archetype tuple. (macro expansion)
type ArchetypeTuple<'a> = (
    &'a str,
    &'a str,
    Vec<&'a dyn FullPage>,
    Box<dyn ContentSourceInternal>,
);