pochoir 0.15.1

Main crate of the pochoir template engine used to compile and render pochoir files with components
Documentation
use crate::{
    component_file::ComponentFile, component_not_found, transformers::Transformers, Context, Result,
};

use std::{
    collections::HashMap,
    path::{Path, PathBuf},
};

/// A template provider taking static strings and storing them in a map with the template name as key.
///
/// It can be used when the templates does not change much, if you have previously parsed the source string
/// representing the template and you want to store it somewhere or if you need complete control over insertion/removal
/// of templates in a single structure.
#[derive(Debug, Clone)]
pub struct StaticMapProvider {
    templates: HashMap<String, (PathBuf, String)>,
}

impl StaticMapProvider {
    /// Create a new empty [`StaticMapProvider`].
    pub fn new() -> Self {
        Self {
            templates: HashMap::new(),
        }
    }

    /// Insert a template into the [`StaticMapProvider`], using the builder pattern.
    ///
    /// It takes as arguments the name of the template (used to include it in other templates), the
    /// text source containing the definition of the template and optionally the path to the file
    /// in the filesystem if it was fetched from the disk (used to show the path in error
    /// messages, otherwise it will just be "inline").
    #[must_use]
    pub fn with_template<A: Into<String>, B: Into<String>>(
        mut self,
        name: A,
        source: B,
        path: Option<&Path>,
    ) -> Self {
        let name = name.into();
        self.templates.insert(
            name,
            (
                path.unwrap_or_else(|| Path::new("inline")).to_owned(),
                source.into(),
            ),
        );
        self
    }

    /// Insert all templates of an existing [`StaticMapProvider`] into this one.
    ///
    /// # Example
    ///
    /// ```
    /// use pochoir::{StaticMapProvider, Context};
    ///
    /// let components = StaticMapProvider::new()
    ///     .with_template("x-title", r#"<h1 class="title"><slot></slot></h1>"#, None)
    ///     .with_template("x-spoil", "<details>
    ///     <summary>spoil</summary>
    ///     <slot></slot>
    /// </details>", None);
    ///
    /// let mut provider = StaticMapProvider::new()
    ///     .with_template("index", "<x-title>A great movie!</x-title><section>
    ///     This movie ends with <x-spoil>a boss being killed</x-spoil>.
    /// </section>", None);
    /// provider.extend(components);
    ///
    /// let compiled = provider.compile("index", &mut Context::new()).expect("failed to compile");
    ///
    /// assert_eq!(compiled, r#"<h1 class="title">A great movie!</h1><section>
    ///     This movie ends with <details>
    ///     <summary>spoil</summary>
    ///     a boss being killed
    /// </details>.
    /// </section>"#);
    /// ```
    pub fn extend(&mut self, other: StaticMapProvider) {
        self.templates.extend(other.templates);
    }

    /// Insert a template into the [`StaticMapProvider`].
    ///
    /// It takes as arguments the name of the template (used to include it in other templates), the
    /// text source containing the definition of the template and optionally the path to the file
    /// in the filesystem if it was fetched from the disk (used to show the path in error
    /// messages, otherwise it will just be "inline").
    pub fn insert<A: Into<String>, B: Into<String>>(
        &mut self,
        name: A,
        source: B,
        path: Option<&Path>,
    ) {
        let name = name.into();
        self.templates.insert(
            name,
            (
                path.unwrap_or_else(|| Path::new("inline")).to_owned(),
                source.into(),
            ),
        );
    }

    /// Get a template previously inserted into the provider.
    ///
    /// # Errors
    ///
    /// Returns `None` if the template is not found.
    pub fn get(&self, name: &str) -> Option<ComponentFile<'_, '_>> {
        self.templates
            .get(name)
            .map(|t| ComponentFile::new(&t.0, &t.1))
    }

    /// Remove a template from the provider.
    ///
    /// Returns the removed template.
    pub fn remove(&mut self, name: &str) -> Option<ComponentFile<'_, '_>> {
        self.templates
            .remove(name)
            .map(|t| ComponentFile::new(t.0, t.1))
    }

    /// Returns an iterator over the name of all inserted templates.
    ///
    /// The order of the names **must not** be considered stable between calls.
    pub fn template_names(&self) -> impl Iterator<Item = &String> {
        self.templates.keys()
    }

    /// Make a component resolver function from the provider, used in the
    /// [`compile`] function.
    ///
    /// [`compile`]: crate::compile
    pub fn to_resolver_fn<'a>(&'a self) -> impl FnMut(&str) -> Result<ComponentFile<'a, 'a>> {
        move |name| {
            if let Some((file_path, data)) = self.templates.get(name) {
                Ok(ComponentFile::new(file_path, data))
            } else {
                Err(component_not_found(name))
            }
        }
    }

    /// Compile the specified component using the specified context.
    ///
    /// Manages the component resolver function under the hood unlike [`compile`].
    ///
    /// # Errors
    ///
    /// It is up to you to format runtime errors (e.g using [`error::display_ansi_error`]
    /// or [`error::display_html_error`]).
    ///
    /// For example, to display the error using ANSI escape codes (to be used in shells) you can
    /// call it like this:
    ///
    /// ```
    /// use pochoir::{StaticMapProvider, Context};
    /// use std::path::Path;
    ///
    /// let source = "<h1>A title</h1>{{ [1, 2]['abc'] }}";
    /// let provider = StaticMapProvider::new()
    ///     .with_template(
    ///         "index",
    ///         source,
    ///         Some(Path::new("src/index.html")),
    ///     );
    ///
    /// let compiled = provider.compile("index", &mut Context::new()).map_err(|e| {
    ///     // Runtime error formatting happens here, it uses
    ///     // `pochoir::common::Spanned::component_name` to get
    ///     // the name of the component which raised the error and
    ///     // fetches it from the provider to get the text source of
    ///     // the component
    ///     pochoir::error::display_ansi_error(
    ///         &e,
    ///         &provider.get(e.component_name())
    ///         .expect("component should not be removed from the provider")
    ///         .data,
    ///     )
    /// });
    ///
    /// assert_eq!(
    ///     compiled,
    ///     Err("\u{1b}[1m\u{1b}[31merror\u{1b}[0m\u{1b}[1m: array cannot be indexed by String, the index must be a positive number or a range\u{1b}[0m\n   src/index.html:1:27\n\n \u{1b}[1m\u{1b}[34m1 |\u{1b}[0m <h1>A title</h1>{{ [1, 2][\u{1b}[1m\u{1b}[31m'abc'\u{1b}[0m] }}".to_string())
    /// );
    /// ```
    ///
    /// [`compile`]: crate::compile
    /// [`error::display_ansi_error`]: crate::error::display_ansi_error
    /// [`error::display_html_error`]: crate::error::display_html_error
    pub fn compile<'a: 'b, 'b>(
        &self,
        default_component_name: &str,
        default_context: &mut Context,
    ) -> Result<String> {
        crate::compile(
            default_component_name,
            default_context,
            self.to_resolver_fn(),
        )
    }

    /// Compile each template while applying transformers.
    ///
    /// See [`transformers`](`crate::transformers`) and [`StaticMapProvider::compile`].
    ///
    /// # Errors
    ///
    /// It is up to you to format runtime errors (e.g using [`error::display_ansi_error`]
    /// or [`error::display_html_error`]).
    ///
    /// For example, to display the error using ANSI escape codes (to be used in shells) you can
    /// call it like this:
    ///
    /// ```
    /// use pochoir::{StaticMapProvider, Context, Transformers};
    /// use std::path::Path;
    ///
    /// let source = "<h1>A title</h1>{{ [1, 2]['abc'] }}";
    /// let provider = StaticMapProvider::new()
    ///     .with_template(
    ///         "index",
    ///         source,
    ///         Some(Path::new("src/index.html")),
    ///     );
    ///
    /// let compiled = provider.transform_and_compile("index", &mut Context::new(), &mut Transformers::new()).map_err(|e| {
    ///     // Runtime error formatting happens here, it uses
    ///     // `pochoir::common::Spanned::component_name` to get
    ///     // the name of the component which raised the error and
    ///     // fetches it from the provider to get the text source of
    ///     // the component
    ///     pochoir::error::display_ansi_error(
    ///         &e,
    ///         &provider.get(e.component_name())
    ///         .expect("component should not be removed from the provider")
    ///         .data,
    ///     )
    /// });
    ///
    /// assert_eq!(
    ///     compiled,
    ///     Err("\u{1b}[1m\u{1b}[31merror\u{1b}[0m\u{1b}[1m: array cannot be indexed by String, the index must be a positive number or a range\u{1b}[0m\n   src/index.html:1:27\n\n \u{1b}[1m\u{1b}[34m1 |\u{1b}[0m <h1>A title</h1>{{ [1, 2][\u{1b}[1m\u{1b}[31m'abc'\u{1b}[0m] }}".to_string())
    /// );
    /// ```
    ///
    /// [`error::display_ansi_error`]: crate::error::display_ansi_error
    /// [`error::display_html_error`]: crate::error::display_html_error
    pub fn transform_and_compile<'a: 'b, 'b>(
        &self,
        default_component_name: &str,
        default_context: &mut Context,
        transformers: &mut Transformers,
    ) -> Result<String> {
        crate::transform_and_compile(
            default_component_name,
            default_context,
            self.to_resolver_fn(),
            transformers,
        )
    }
}

impl Default for StaticMapProvider {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use crate::{error::SpannedWithComponent, Error};

    use super::*;

    // AssertSendSync is a trait used to make sure `Send + Sync` is implemented on the `StaticMapProvider`
    #[allow(dead_code)]
    trait AssertSendSync: Send + Sync {}
    impl AssertSendSync for StaticMapProvider {}

    #[test]
    fn compile_basic() {
        let mut provider = StaticMapProvider::new().with_template(
            "index",
            "<h1>A title</h1><about-page />",
            Some(Path::new("src/index.html")),
        );
        provider.insert(
            "about-page",
            "<h1>About page</h1>",
            Some(Path::new("src/about.html")),
        );

        assert_eq!(
            provider.compile("index", &mut Context::new()),
            Ok("<h1>A title</h1><h1>About page</h1>".to_string())
        );
    }

    #[test]
    fn compile_low_level() {
        let mut provider = StaticMapProvider::new().with_template(
            "index",
            "<h1>A title</h1><about-page />",
            Some(Path::new("src/index.html")),
        );
        provider.insert(
            "about-page",
            "<h1>About page</h1>",
            Some(Path::new("src/about.html")),
        );

        assert_eq!(
            crate::compile("index", &mut Context::new(), provider.to_resolver_fn()),
            Ok("<h1>A title</h1><h1>About page</h1>".to_string())
        );
    }

    #[test]
    fn errors() {
        let provider = StaticMapProvider::new().with_template(
            "index",
            "<h1>A title</",
            Some(Path::new("src/index.html")),
        );

        assert_eq!(
            **provider.compile("index", &mut Context::new()).unwrap_err(),
            Error::ParserError(crate::parser::Error::ExpectedTagName)
        );

        let provider =
            StaticMapProvider::new().with_template("index", "<h1>A title</h1><about-page />", None);

        assert_eq!(
            crate::compile("index", &mut Context::new(), provider.to_resolver_fn()),
            Err(SpannedWithComponent::new(Error::ComponentNotFound {
                name: "about-page".to_string()
            })
            .with_file_path("inline")
            .with_span(16..30)
            .with_component_name("index")),
        );
    }
}