// Copyright (C) 2016, 2017, 2018 Élisabeth HENRY.
//
// This file is part of Crowbook.
//
// Crowbook is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation, either version 2.1 of the License, or
// (at your option) any later version.
//
// Crowbook is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with Crowbook.  If not, see <http://www.gnu.org/licenses/>.

use crate::book_bars::Bars;
use crate::book_renderer::BookRenderer;
use crate::bookoptions::BookOptions;
use crate::chapter::Chapter;
use crate::cleaner::{Cleaner, CleanerParams, Default, French, Off};
use crate::epub::Epub;
use crate::error::{Error, Result, Source};
use crate::html_dir::{HtmlDir, ProofHtmlDir};
use crate::html_if::HtmlIf;
use crate::html_single::{HtmlSingle, ProofHtmlSingle};
use crate::lang;
use crate::latex::{Latex, Pdf, ProofLatex, ProofPdf};
use crate::misc;
use crate::number::Number;
#[cfg(feature = "odt")]
use crate::odt::Odt;
use crate::parser::Features;
use crate::parser::Parser;
use crate::resource_handler::ResourceHandler;
use crate::templates::{epub, epub3, highlight, html, html_dir, html_if, html_single, latex};
use crate::text_view::view_as_text;
use crate::token::Token;

#[cfg(feature = "proofread")]
use crate::grammalecte::GrammalecteChecker;
#[cfg(feature = "proofread")]
use crate::grammar_check::GrammarChecker;
#[cfg(feature = "proofread")]
use crate::repetition_check::RepetitionDetector;
// Dummy grammarchecker thas does nothing to let the compiler compile
#[cfg(not(feature = "proofread"))]
struct GrammarChecker {}
#[cfg(not(feature = "proofread"))]
impl GrammarChecker {
    fn check_chapter(&self, _: &[Token]) -> Result<()> {
        Ok(())
    }
}
// Dummy grammalectechecker thas does nothing to let the compiler compile
#[cfg(not(feature = "proofread"))]
struct GrammalecteChecker {}
#[cfg(not(feature = "proofread"))]
impl GrammalecteChecker {
    fn check_chapter(&self, _: &[Token]) -> Result<()> {
        Ok(())
    }
}
// Dummy RepetitionDetector thas does nothing to let the compiler compile
#[cfg(not(feature = "proofread"))]
struct RepetitionDetector {}
#[cfg(not(feature = "proofread"))]
impl RepetitionDetector {
    fn check_chapter(&self, _: &[Token]) -> Result<()> {
        Ok(())
    }
}

use std::borrow::Cow;
use std::cmp::Ordering;
use std::collections::HashMap;
use std::fmt;
use std::fs::File;
use std::io::{Read, Write};
use std::iter::IntoIterator;
use std::path::{Path, PathBuf};

use mustache::{MapBuilder, Template};
use numerals::roman::Roman;
use rayon::prelude::*;
use yaml_rust::{Yaml, YamlLoader};

/// Type of header (part or chapter)
#[derive(Copy, Clone, Debug)]
pub enum Header {
    /// Chapter (default)
    Chapter,
    /// Part (or "book" or "episode" or whatever)
    Part,
}

/// Header data (for chapter or part)
#[derive(Debug, Clone)]
pub struct HeaderData {
    /// A string containnig the full text version, for e.g. TOCs
    pub text: String,
    /// The title of the header, e.g. "Part" or "Chapter" or nothing
    pub header: String,
    /// The number, formatted in roman or arabic
    pub number: String,
    /// Only the title
    pub title: String,
}

/// The types of bars
#[derive(Copy, Clone)]
pub enum Crowbar {
    Main,
    Second,
    Spinner(usize),
}

/// The state of bars
#[derive(Copy, Clone)]
pub enum CrowbarState {
    Running,
    Success,
    Error,
}

impl fmt::Display for HeaderData {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.text)
    }
}

/// A Book.
///
/// Probably the central structure for of Crowbook, as it is the one
/// that calls the other ones.
///
/// It has the tasks of loading a configuration file, loading chapters
/// and using `Parser`to parse them, and then calling various renderers
/// (`HtmlRendrer`, `LatexRenderer`, `EpubRenderer` and/or `OdtRenderer`)
/// to convert the AST into documents.
///
/// # Examples
///
/// ```
/// use crowbook::{Book, Number};
/// // Create a book with some options
/// let mut book = Book::new();
/// book.set_options(&[("author", "Joan Doe"),
///                    ("title", "An untitled book"),
///                    ("lang", "en")]);
///
/// // Add a chapter to the book
/// book.add_chapter_from_source(Number::Default, "# The beginning#\nBla, bla, bla".as_bytes()).unwrap();
///
/// // Render the book as html to stdout
/// book.render_format_to("html", &mut std::io::stdout()).unwrap();
/// ```
pub struct Book {
    /// Internal structure. You should not accesss this directly except if
    /// you are writing a new renderer.
    pub chapters: Vec<Chapter>,

    /// Options of the book
    pub options: BookOptions,

    /// Root path of the book
    #[doc(hidden)]
    pub root: PathBuf,

    /// Source for error files
    #[doc(hidden)]
    pub source: Source,

    /// Features used in the book content
    #[doc(hidden)]
    pub features: Features,

    cleaner: Box<dyn Cleaner>,
    chapter_template: Option<Template>,
    part_template: Option<Template>,
    checker: Option<GrammarChecker>,
    grammalecte: Option<GrammalecteChecker>,
    detector: Option<RepetitionDetector>,
    formats: HashMap<&'static str, (String, Box<dyn BookRenderer>)>,

    #[doc(hidden)]
    pub bars: Bars,
}

impl Book {
    /// Creates a new, empty `Book`
    pub fn new() -> Book {
        let mut book = Book {
            source: Source::empty(),
            chapters: vec![],
            cleaner: Box::new(Off),
            root: PathBuf::new(),
            options: BookOptions::new(),
            chapter_template: None,
            part_template: None,
            checker: None,
            grammalecte: None,
            detector: None,
            formats: HashMap::new(),
            features: Features::new(),
            bars: Bars::new(),
        };
        book.add_format(
            "html",
            lformat!("HTML (standalone page)"),
            Box::new(HtmlSingle {}),
        )
        .add_format(
            "proofread.html",
            lformat!("HTML (standalone page/proofreading)"),
            Box::new(ProofHtmlSingle {}),
        )
        .add_format(
            "html.dir",
            lformat!("HTML (multiple pages)"),
            Box::new(HtmlDir {}),
        )
        .add_format(
            "proofread.html.dir",
            lformat!("HTML (multiple pages/proofreading)"),
            Box::new(ProofHtmlDir {}),
        )
        .add_format("tex", lformat!("LaTeX"), Box::new(Latex {}))
        .add_format(
            "proofread.tex",
            lformat!("LaTeX (proofreading)"),
            Box::new(ProofLatex {}),
        )
        .add_format("pdf", lformat!("PDF"), Box::new(Pdf {}))
        .add_format(
            "proofread.pdf",
            lformat!("PDF (proofreading)"),
            Box::new(ProofPdf {}),
        )
        .add_format("epub", lformat!("EPUB"), Box::new(Epub {}))
        .add_format(
            "html.if",
            lformat!("HTML (interactive fiction)"),
            Box::new(HtmlIf {}),
        );
        #[cfg(feature = "odt")]
        book.add_format("odt", lformat!("ODT"), Box::new(Odt {}));
        book
    }

    /// Sets an error message to the progress bar, if it is set
    pub fn set_error(&self, msg: &str) {
        self.bar_finish(Crowbar::Main, CrowbarState::Error, msg)
    }

    /// Adds a progress bar where where info should be written.
    ///
    /// See [indicatif doc](https://docs.rs/indicatif) for more information.
    pub fn add_progress_bar(&mut self, emoji: bool) {
        self.private_add_progress_bar(emoji);
    }

    /// Register a format that can be rendered.
    ///
    /// The renderer for this format must implement the `BookRenderer` trait.
    ///
    /// # Example
    ///
    /// ```
    /// use crowbook::{Result, Book, BookRenderer};
    /// use std::io::Write;
    /// struct Dummy {}
    /// impl BookRenderer for Dummy {
    ///     fn render(&self, book: &Book, to: &mut Write) -> Result<()> {
    ///         write!(to, "This does nothing useful").unwrap();
    ///         Ok(())
    ///      }
    /// }
    ///
    /// let mut book = Book::new();
    /// book.add_format("foo",
    ///                 "Some dummy implementation",
    ///                 Box::new(Dummy{}));
    /// ```
    pub fn add_format<S: Into<String>>(
        &mut self,
        format: &'static str,
        description: S,
        renderer: Box<dyn BookRenderer>,
    ) -> &mut Self {
        self.formats.insert(format, (description.into(), renderer));
        self
    }

    /// Sets the options of a `Book`
    ///
    /// # Arguments
    /// * `options`: a (possibly empty) list (or other iterator) of (key, value) tuples.
    ///
    /// # Example
    ///
    /// ```
    /// use crowbook::Book;
    /// let mut book = Book::new();
    /// book.set_options(&[("author", "Foo"), ("title", "Bar")]);
    /// assert_eq!(book.options.get_str("author").unwrap(), "Foo");
    /// assert_eq!(book.options.get_str("title").unwrap(), "Bar");
    /// ```
    pub fn set_options<'a, I>(&mut self, options: I) -> &mut Book
    where
        I: IntoIterator<Item = &'a (&'a str, &'a str)>,
    {
        // set options
        for &(key, value) in options {
            if let Err(err) = self.options.set(key, value) {
                error!(
                    "{}",
                    lformat!(
                        "Error initializing book: could not set {key} to {value}: {error}",
                        key = key,
                        value = value,
                        error = err
                    )
                );
            }
        }
        // set cleaner according to lang and autoclean settings
        self.update_cleaner();
        self
    }

    /// Loads a book configuration file
    ///
    /// # Argument
    /// * `path`: the path of the file to load. The directory of this file is used as
    ///   a "root" directory for all paths referenced in books, whether chapter files,
    ///   templates, cover images, and so on.
    ///
    /// # Example
    ///
    /// ```
    /// # use crowbook::Book;
    /// let mut book = Book::new();
    /// let result = book.load_file("some.book");
    /// ```
    pub fn load_file<P: AsRef<Path>>(&mut self, path: P) -> Result<&mut Book> {
        let filename = format!("{}", path.as_ref().display());
        self.source = Source::new(filename.as_str());
        self.options.source = Source::new(filename.as_str());

        let f = File::open(path.as_ref()).map_err(|_| {
            Error::file_not_found(Source::empty(), lformat!("book"), filename.clone())
        })?;
        // Set book path to book's directory
        if let Some(parent) = path.as_ref().parent() {
            self.root = parent.to_owned();
            self.options.root = self.root.clone();
        }

        let result = self.read_config(&f);
        match result {
            Ok(book) => Ok(book),
            Err(err) => {
                if err.is_config_parser() && path.as_ref().ends_with(".md") {
                    let err = Error::default(
                        Source::empty(),
                        lformat!(
                            "could not parse {file} as a book \
                                                       file.\nMaybe you meant to run crowbook \
                                                       with the --single argument?",
                            file = misc::normalize(path)
                        ),
                    );
                    Err(err)
                } else {
                    Err(err)
                }
            }
        }
    }

    /// Loads a single markdown file
    ///
    /// This is *not* used to add a chapter to an existing book, but to to load the
    /// book configuration file from a single Markdown file.
    ///
    /// Since it is designed for single-chapter short stories, this method also sets
    /// the `tex.class` option to `article`.
    ///
    /// # Example
    ///
    /// ```
    /// use crowbook::Book;
    /// let mut book = Book::new();
    /// book.load_markdown_file("foo.md"); // not unwraping since foo.md doesn't exist
    /// ```
    pub fn load_markdown_file<P: AsRef<Path>>(&mut self, path: P) -> Result<&mut Self> {
        let filename = format!("{}", path.as_ref().display());
        self.source = Source::new(filename.as_str());

        // Set book path to book's directory
        if let Some(parent) = path.as_ref().parent() {
            self.root = parent.to_owned();
            self.options.root = self.root.clone();
        }
        self.options.set("tex.class", "article").unwrap();
        self.options.set("input.yaml_blocks", "true").unwrap();

        // Add the file as chapter with hidden title
        // hideous line, but basically transforms foo/bar/baz.md to baz.md
        let relative_path = Path::new(path.as_ref().components().last().unwrap().as_os_str());

        // Update grammar checker according to options
        self.add_chapter(Number::Hidden, &relative_path.to_string_lossy(), false)?;

        Ok(self)
    }

    /// Reads a single markdown config from a `Read`able object.
    ///
    /// Similar to `load_markdown_file`, except it reads a source instead of a file.
    ///
    /// # Example
    ///
    /// ```
    /// use crowbook::Book;
    /// let content = "\
    /// ---
    /// author: Foo
    /// title: Bar
    /// ---
    ///
    /// # Book #
    ///
    /// Some content in *markdown*.";
    ///
    /// let mut book = Book::new();
    /// book.read_markdown_config(content.as_bytes()).unwrap();
    /// assert_eq!(book.options.get_str("title").unwrap(), "Bar");
    /// ```
    pub fn read_markdown_config<R: Read>(&mut self, source: R) -> Result<&mut Self> {
        self.options.set("tex.class", "article").unwrap();
        self.options.set("input.yaml_blocks", "true").unwrap();

        // Update grammar checker according to options
        self.add_chapter_from_source(Number::Hidden, source, false)?;

        Ok(self)
    }

    /// Sets options from a YAML block
    fn set_options_from_yaml(&mut self, yaml: &str) -> Result<&mut Book> {
        self.options.source = self.source.clone();
        match YamlLoader::load_from_str(yaml) {
            Err(err) => {
                return Err(Error::config_parser(
                    &self.source,
                    lformat!("YAML block was not valid YAML: {error}", error = err),
                ))
            }
            Ok(mut docs) => {
                if docs.len() == 1 && docs[0].as_hash().is_some() {
                    if let Yaml::Hash(hash) = docs.pop().unwrap() {
                        for (key, value) in hash {
                            if let Err(err) = self.options.set_yaml(key, value) {
                                error!("{}", err);
                            };
                        }
                    } else {
                        unreachable!();
                    }
                } else {
                    return Err(Error::config_parser(
                        &self.source,
                        lformat!(
                            "YAML part of the book is not a \
                                                              valid hashmap"
                        ),
                    ));
                }
            }
        }
        Ok(self)
    }

    /// Reads a book configuration from a `Read`able source.
    ///
    /// # Book configuration
    ///
    /// A line with "option: value" sets the option to value
    ///
    /// + chapter_name.md adds the (default numbered) chapter
    ///
    /// - chapter_name.md adds the (unnumbered) chapter
    ///
    /// 3. chapter_name.md adds the (custom numbered) chapter
    ///
    /// # See also
    /// * `load_file`
    ///
    /// # Example
    ///
    /// ```
    /// use crowbook::Book;
    /// let content = "\
    /// author: Foo
    /// title: Bar
    ///
    /// ! intro.md
    /// + chapter_01.md";
    ///
    /// let mut book = Book::new();
    /// book.read_config(content.as_bytes()); // no unwraping as `intro.md` and `chapter_01.md` don't exist
    /// ```
    pub fn read_config<R: Read>(&mut self, mut source: R) -> Result<&mut Book> {
        fn get_filename<'a>(source: &Source, s: &'a str) -> Result<&'a str> {
            let words: Vec<&str> = (&s[1..]).split_whitespace().collect();
            if words.len() > 1 {
                return Err(Error::config_parser(
                    source,
                    lformat!(
                        "chapter filenames must not contain \
                                                          whitespace"
                    ),
                ));
            } else if words.is_empty() {
                return Err(Error::config_parser(
                    source,
                    lformat!("no chapter name specified"),
                ));
            }
            Ok(words[0])
        }

        self.bar_set_message(Crowbar::Main, &lformat!("setting options"));

        let mut s = String::new();
        source.read_to_string(&mut s).map_err(|err| {
            Error::config_parser(
                Source::empty(),
                lformat!("could not read source: {error}", error = err),
            )
        })?;

        // Parse the YAML block, that is, until first chapter
        let mut yaml = String::new();
        let mut lines = s.lines().peekable();
        let mut line;

        let mut line_number = 0;
        let mut is_next_line_ok: bool;

        loop {
            if let Some(next_line) = lines.peek() {
                if next_line.starts_with(|c| match c {
                    '-' | '+' | '!' | '@' => true,
                    _ => c.is_digit(10),
                }) {
                    break;
                }
            } else {
                break;
            }
            line = lines.next().unwrap();
            line_number += 1;
            self.source.set_line(line_number);
            yaml.push_str(line);
            yaml.push('\n');

            if line
                .trim()
                .ends_with(|c| matches!(c, '>' | '|' | ':' | '-'))
            {
                // line ends with the start of a block indicator
                continue;
            }

            if let Some(next_line) = lines.peek() {
                let doc = YamlLoader::load_from_str(next_line);
                if doc.is_err() {
                    is_next_line_ok = false;
                } else {
                    let doc = doc.unwrap();
                    if !doc.is_empty() && doc[0].as_hash().is_some() {
                        is_next_line_ok = true;
                    } else {
                        is_next_line_ok = false;
                    }
                }
            } else {
                break;
            }
            if !is_next_line_ok {
                // If next line is not valid yaml, probably means we are in a multistring
                continue;
            }
            let result = self.set_options_from_yaml(&yaml);
            match result {
                Ok(_) => {
                    // Fine, we can remove previous lines
                    yaml = String::new();
                }
                Err(err) => {
                    if err.is_book_option() {
                        // book option error: abort
                        return Err(err);
                    } else {
                        // Other error: we do nothing, hoping it will work
                        // itself out when more lines are added to yaml
                    }
                }
            }
        }
        self.set_options_from_yaml(&yaml)?;

        // Update cleaner according to options (autoclean/lang)
        self.update_cleaner();

        // Update grammar checker according to options (proofread.*)
        self.init_checker();

        self.bar_set_message(Crowbar::Main, &lformat!("Parsing chapters"));

        // Parse chapters
        let lines: Vec<_> = lines.collect();
        self.add_second_bar(&lformat!("Processing..."), lines.len() as u64);
        for line in lines {
            self.inc_second_bar();
            line_number += 1;
            self.source.set_line(line_number);
            let line = line.trim();
            if line.is_empty() || line.starts_with('#') {
                continue;
            }
            if line.starts_with("--") {
                // Subchapter
                let mut level = 0;
                for b in line.bytes() {
                    if b == b'-' {
                        level += 1;
                    } else {
                        break;
                    }
                }
                assert!(level > 1);
                level -= 1;
                let file = get_filename(&self.source, &line[level..])?;
                self.add_subchapter(level as i32, file)?;
            } else if line.starts_with('-') {
                // unnumbered chapter
                let file = get_filename(&self.source, line)?;
                self.add_chapter(Number::Unnumbered, file, false)?;
            } else if line.starts_with('+') {
                // numbered chapter
                let file = get_filename(&self.source, line)?;
                self.add_chapter(Number::Default, file, true)?;
            } else if line.starts_with('!') {
                // hidden chapter
                let file = get_filename(&self.source, line)?;
                self.add_chapter(Number::Hidden, file, false)?;
            } else if line.starts_with(|c: char| c.is_digit(10)) {
                // chapter with specific number
                let parts: Vec<_> = line
                    .splitn(2, |c: char| c == '.' || c == ':' || c == '+')
                    .collect();
                if parts.len() != 2 {
                    return Err(Error::config_parser(
                        &self.source,
                        lformat!(
                            "ill-formatted line specifying \
                                                              chapter number"
                        ),
                    ));
                }
                let file = get_filename(&self.source, parts[1])?;
                let number = parts[0].parse::<i32>().map_err(|err| {
                    Error::config_parser(
                        &self.source,
                        lformat!("error parsing chapter number: {error}", error = err),
                    )
                })?;
                self.add_chapter(Number::Specified(number), file, true)?;
            } else if line.starts_with('@') {
                /* Part */
                let subline = &line[1..];
                if subline.starts_with(|c: char| c.is_whitespace()) {
                    let subline = subline.trim();
                    let ast = Parser::from(self).parse_inline(subline)?;
                    let ast = vec![Token::Header(1, ast)];
                    self.chapters
                        .push(Chapter::new(Number::DefaultPart, String::new(), ast));
                } else if subline.starts_with('-') {
                    /* Unnumbered part */
                    let file = get_filename(&self.source, subline)?;
                    self.add_chapter(Number::UnnumberedPart, file, true)?;
                } else if subline.starts_with('+') {
                    /* Numbered part */
                    let file = get_filename(&self.source, subline)?;
                    self.add_chapter(Number::DefaultPart, file, true)?;
                } else if subline.starts_with(|c: char| c.is_digit(10)) {
                    /* Specified  part*/
                    let parts: Vec<_> = subline
                        .splitn(2, |c: char| c == '.' || c == ':' || c == '+')
                        .collect();
                    if parts.len() != 2 {
                        return Err(Error::config_parser(
                            &self.source,
                            lformat!(
                                "ill-formatted line specifying \
                                                                  part number"
                            ),
                        ));
                    }
                    let file = get_filename(&self.source, parts[1])?;
                    let number = parts[0].parse::<i32>().map_err(|err| {
                        Error::config_parser(
                            &self.source,
                            lformat!("error parsing part number: {error}", error = err),
                        )
                    })?;
                    self.add_chapter(Number::SpecifiedPart(number), file, true)?;
                } else {
                    return Err(Error::config_parser(
                        &self.source,
                        lformat!("found invalid part definition in the chapter list"),
                    ));
                }
            } else {
                return Err(Error::config_parser(
                    &self.source,
                    lformat!(
                        "found invalid chapter definition in \
                                                          the chapter list"
                    ),
                ));
            }
        }

        self.bar_finish(Crowbar::Second, CrowbarState::Success, "");

        self.source.unset_line();
        self.set_chapter_template()?;
        Ok(self)
    }

    /// Determine whether proofreading is activated or not
    fn is_proofread(&self) -> bool {
        self.options.get_bool("proofread").unwrap()
            && (self.options.get("output.proofread.html").is_ok()
                || self.options.get("output.proofread.html.dir").is_ok()
                || self.options.get("output.proofread.pdf").is_ok())
    }

    /// Initialize the grammar checker and repetetion detector if they needs to be
    #[cfg(feature = "proofread")]
    fn init_checker(&mut self) {
        if self.is_proofread() {
            if self.options.get_bool("proofread.languagetool").unwrap() {
                let port = self.options.get_i32("proofread.languagetool.port").unwrap() as usize;
                let lang = self.options.get_str("lang").unwrap();
                let checker = GrammarChecker::new(port, lang);
                match checker {
                    Ok(checker) => self.checker = Some(checker),
                    Err(e) => {
                        error!(
                            "{}",
                            lformat!("{error}. Proceeding without using languagetool.", error = e)
                        )
                    }
                }
            }
            if self.options.get_bool("proofread.grammalecte").unwrap() {
                let port = self.options.get_i32("proofread.grammalecte.port").unwrap() as usize;
                let lang = self.options.get_str("lang").unwrap();
                let checker = GrammalecteChecker::new(port, lang);
                match checker {
                    Ok(checker) => self.grammalecte = Some(checker),
                    Err(e) => {
                        error!(
                            "{}",
                            lformat!("{error}. Proceeding without using grammalecte.", error = e)
                        )
                    }
                }
            }
            if self.options.get_bool("proofread.repetitions").unwrap() {
                self.detector = Some(RepetitionDetector::new(self));
            }
        }
    }

    #[cfg(not(feature = "proofread"))]
    fn init_checker(&mut self) {}

    /// Renders the book to the given format if output.{format} is set;
    /// do nothing otherwise.
    ///
    /// # Example
    ///
    /// ```
    /// use crowbook::Book;
    /// let mut book = Book::new();
    /// /* Will do nothing as book is empty and has no output format specified */
    /// book.render_format("pdf");
    /// ```
    pub fn render_format(&self, format: &str) {
        // TODO: check that it doesn't break everything, or use option?
        self.render_format_with_bar(format, 0)
    }

    /// Generates output files acccording to book options.
    ///
    /// # Example
    ///
    /// ```
    /// use crowbook::Book;
    /// let content = "\
    /// ---
    /// title: Foo
    /// output.tex: /tmp/foo.tex
    /// ---
    ///
    /// # Foo
    ///
    /// Bar and baz, too.";
    ///
    /// Book::new()
    ///       .read_markdown_config(content.as_bytes())
    ///       .unwrap()
    ///       .render_all(); // renders foo.tex in /tmp
    /// ```
    pub fn render_all(&mut self) {
        let mut keys: Vec<_> = self
            .formats
            .keys()
            .filter(|fmt| {
                if !self.is_proofread() && fmt.contains("proofread") {
                    return false;
                }
                self.options.get_path(&format!("output.{}", fmt)).is_ok()
            })
            .map(|s| s.to_string())
            .collect();
        // Make sure that PDF comes first since running latex takes lots of time
        keys.sort_by(|fmt1, fmt2| {
            if fmt1.contains("pdf") {
                Ordering::Less
            } else if fmt2.contains("pdf") {
                Ordering::Greater
            } else {
                Ordering::Equal
            }
        });

        for key in &keys {
            self.add_spinner_to_multibar(key);
        }

        keys.par_iter().enumerate().for_each(|(i, fmt)| {
            self.render_format_with_bar(fmt, i);
        });

        self.bar_finish(Crowbar::Main, CrowbarState::Success, &lformat!("Finished"));

        // if handles.is_empty() {
        //     Logger::display_warning(lformat!("Crowbook generated no file because no output file was \
        //                              specified. Add output.{{format}} to your config file."));
        // }
    }

    /// Renders the book to the given format and reports to progress bar if set
    pub fn render_format_with_bar(&self, format: &str, bar: usize) {
        let mut key = String::from("output.");
        key.push_str(format);
        if let Ok(path) = self.options.get_path(&key) {
            self.bar_set_message(Crowbar::Spinner(bar), &lformat!("rendering..."));
            let result = self.render_format_to_file_with_bar(format, path, bar);
            if let Err(err) = result {
                self.bar_finish(
                    Crowbar::Spinner(bar),
                    CrowbarState::Error,
                    &format!("{}", err),
                );
                error!(
                    "{}",
                    lformat!(
                        "Error rendering {name}: {error}",
                        name = format,
                        error = err
                    )
                );
            }
        }
    }

    pub fn render_format_to_file_with_bar<P: Into<PathBuf>>(
        &self,
        format: &str,
        path: P,
        bar: usize,
    ) -> Result<()> {
        debug!(
            "{}",
            lformat!("Attempting to generate {format}...", format = format)
        );
        let path = path.into();
        match self.formats.get(format) {
            Some(&(ref description, ref renderer)) => {
                let path = if path.ends_with("auto") {
                    let file = if let Some(s) = self
                        .source
                        .file
                        .as_ref()
                        .and_then(|f| Path::new(f).file_stem())
                    {
                        s.to_string_lossy().into_owned()
                    } else {
                        return Err(Error::default(&self.source, lformat!("output to {format} set to auto but can't find book file name to infer it",
                                                                     format = description)));
                    };
                    let file = renderer.auto_path(&file).map_err(|_| {
                        Error::default(
                            &self.source,
                            lformat!(
                                "the {format} renderer does not support auto for output path",
                                format = description
                            ),
                        )
                    })?;
                    path.with_file_name(file)
                } else {
                    path
                };
                renderer.render_to_file(self, &path)?;
                let path = misc::normalize(path);
                let msg = lformat!(
                    "Succesfully generated {format}: {path}",
                    format = description,
                    path = &path
                );
                info!("{}", &msg);
                self.bar_finish(
                    Crowbar::Spinner(bar),
                    CrowbarState::Success,
                    &lformat!("generated {path}", path = path),
                );
                Ok(())
            }
            None => Err(Error::default(
                Source::empty(),
                lformat!("unknown format {format}", format = format),
            )),
        }
    }

    /// Render book to specified format according to book options, and write the results
    /// in the `Write` object.
    ///
    /// This method will fail if the format is not handled by the book, or if there is a
    /// problem during rendering, or if the renderer can't render to a byte stream (e.g.
    /// multiple files HTML renderer can't, as it must create a directory.)
    ///
    /// # See also
    /// * `render_format_to_file`, which creates a new file (that *can* be a directory).
    /// * `render_format`, which won't do anything if `output.{format}` isn't specified
    ///   in the book configuration file.
    pub fn render_format_to<T: Write>(&mut self, format: &str, f: &mut T) -> Result<()> {
        debug!(
            "{}",
            lformat!("Attempting to generate {format}...", format = format)
        );
        let bar = self.add_spinner_to_multibar(format);
        match self.formats.get(format) {
            Some(&(ref description, ref renderer)) => match renderer.render(self, f) {
                Ok(_) => {
                    self.bar_finish(
                        Crowbar::Spinner(bar),
                        CrowbarState::Success,
                        &lformat!("generated {format}", format = format),
                    );
                    self.bar_finish(Crowbar::Main, CrowbarState::Success, &lformat!("Finished"));
                    info!(
                        "{}",
                        lformat!("Succesfully generated {format}", format = description)
                    );
                    Ok(())
                }
                Err(e) => {
                    self.bar_finish(
                        Crowbar::Spinner(bar),
                        CrowbarState::Error,
                        &lformat!("{error}", error = e),
                    );
                    self.bar_finish(Crowbar::Main, CrowbarState::Error, &lformat!("ERROR"));
                    Err(e)
                }
            },
            None => {
                self.bar_finish(
                    Crowbar::Spinner(bar),
                    CrowbarState::Error,
                    &lformat!("unknown format"),
                );
                Err(Error::default(
                    Source::empty(),
                    lformat!("unknown format {format}", format = format),
                ))
            }
        }
    }

    /// Render book to specified format according to book options. Creates a new file
    /// and write the result in it.
    ///
    /// This method will fail if the format is not handled by the book, or if there is a
    /// problem during rendering.
    ///
    /// # Arguments
    ///
    /// * `format`: the format to render;
    /// * `path`: a path to the file that will be created;
    /// * `bar`: a Progressbar, or `None`
    ///
    /// # See also
    /// * `render_format_to`, which writes in any `Write`able object.
    /// * `render_format`, which won't do anything if `output.{format}` isn't specified
    ///   in the book configuration file.

    pub fn render_format_to_file<P: Into<PathBuf>>(&mut self, format: &str, path: P) -> Result<()> {
        let bar = self.add_spinner_to_multibar(format);
        let path = path.into();
        let normalized = misc::normalize(&path);
        self.render_format_to_file_with_bar(format, path, bar)?;
        self.bar_finish(
            Crowbar::Spinner(bar),
            CrowbarState::Success,
            &lformat!("generated {path}", path = normalized),
        );
        self.bar_finish(Crowbar::Main, CrowbarState::Success, &lformat!("Finished"));
        Ok(())
    }

    /// Adds a chapter to the book.
    ///
    /// This method is the backend used both by `add_chapter` and `add_chapter_from_source`.
    pub fn add_chapter_from_named_source<R: Read>(
        &mut self,
        number: Number,
        file: &str,
        mut source: R,
        mut add_title_if_empty: bool,
    ) -> Result<&mut Self> {
        self.bar_set_message(
            Crowbar::Main,
            &lformat!("Processing {file}...", file = file),
        );
        let mut content = String::new();
        source.read_to_string(&mut content).map_err(|_| {
            Error::parser(
                &self.source,
                lformat!(
                    "file {file} contains invalid UTF-8",
                    file = misc::normalize(file)
                ),
            )
        })?;


        // parse the file
        self.bar_set_message(Crowbar::Second, &lformat!("Parsing..."));

        let mut parser = Parser::from(self);
        parser.set_source_file(file);
        let mut yaml_block = String::from("");
        let mut tokens = parser.parse(&content, Option::Some(&mut yaml_block))?;
        
        // Parse YAML block
        self.parse_yaml(&yaml_block);
        self.features = self.features | parser.features();

        // transform the AST to make local links and images relative to `book` directory
        let offset = if let Some(f) = Path::new(file).parent() {
            f
        } else {
            Path::new("")
        };
        if offset.starts_with("..") {
            debug!(
                "{}",
                lformat!(
                    "Warning: book contains chapter '{file}' in a directory above \
                                   the book file, this might cause problems",
                    file = misc::normalize(file)
                )
            );
        }

        // For offset: if nothing is specified, it is the filename's directory
        // If base_path.{images/links} is specified, override it for one of them.
        // If base_path is specified, override it for both.
        let res_base = self.options.get_path("resources.base_path");
        let res_base_img = self.options.get_path("resources.base_path.images");
        let res_base_lnk = self.options.get_path("resources.base_path.links");
        let mut link_offset = offset;
        let mut image_offset = offset;
        if let Ok(ref path) = res_base {
            link_offset = Path::new(path);
            image_offset = Path::new(path);
        } else {
            if let Ok(ref path) = res_base_img {
                image_offset = Path::new(path);
            }
            if let Ok(ref path) = res_base_lnk {
                link_offset = Path::new(path);
            }
        }
        // add offset
        ResourceHandler::add_offset(link_offset, image_offset, &mut tokens);

        // If files_mean_chapters is set, override the default setting
        if let Ok(x) = self.options.get_bool("crowbook.files_mean_chapters") {
            add_title_if_empty = x;
        }

        // Add a title if there is none in the chapter (unless this is subchapter)
        if add_title_if_empty {
            misc::insert_title(&mut tokens);
        }

        // If one of the renderers requires it, perform grammarcheck
        if cfg!(feature = "proofread") && self.is_proofread() {
            let normalized = misc::normalize(file);
            if let Some(ref checker) = self.checker {
                self.bar_set_message(Crowbar::Second, &lformat!("Running languagetool"));

                info!(
                    "{}",
                    lformat!(
                        "Trying to run languagetool on {file}, this might take a \
                                    while...",
                        file = &normalized
                    )
                );
                if let Err(err) = checker.check_chapter(&mut tokens) {
                    error!(
                        "{}",
                        lformat!(
                            "Error running languagetool on {file}: {error}",
                            file = &normalized,
                            error = err
                        )
                    );
                }
            }
            if let Some(ref checker) = self.grammalecte {
                self.bar_set_message(Crowbar::Second, &lformat!("Running grammalecte"));

                info!(
                    "{}",
                    lformat!(
                        "Trying to run grammalecte on {file}, this might take a \
                                    while...",
                        file = &normalized
                    )
                );
                if let Err(err) = checker.check_chapter(&mut tokens) {
                    error!(
                        "{}",
                        lformat!(
                            "Error running grammalecte on {file}: {error}",
                            file = &normalized,
                            error = err
                        )
                    );
                }
            }
            if let Some(ref detector) = self.detector {
                self.bar_set_message(Crowbar::Second, &lformat!("Detecting repetitions"));

                info!(
                    "{}",
                    lformat!(
                        "Trying to run repetition detector on {file}, this might take a \
                                      while...",
                        file = &normalized
                    )
                );
                if let Err(err) = detector.check_chapter(&mut tokens) {
                    error!(
                        "{}",
                        lformat!(
                            "Error running repetition detector on {file}: {error}",
                            file = &normalized,
                            error = err
                        )
                    );
                }
            }
        }
        self.bar_set_message(Crowbar::Second, "");

        self.chapters.push(Chapter::new(number, file, tokens));

        Ok(self)
    }

    /// Adds a chapter, as a file name, to the book
    pub fn add_subchapter(&mut self, level: i32, file: &str) -> Result<&mut Self> {
        let number = {
            if let Some(chapter) = self.chapters.last() {
                chapter.number
            } else {
                Number::Hidden
            }
        };
        self.add_chapter(number, file, false)?;

        // Adjust header levels
        {
            let last = self.chapters.last_mut().unwrap();
            for token in &mut last.content {
                match *token {
                    Token::Header(ref mut n, _) => {
                        let new = *n + level;
                        if !(0..=6).contains(&new) {
                            return Err(Error::parser(Source::new(file),
                                                     lformat!("this subchapter contains a heading that, when adjusted, is not in the right range ({} instead of [0-6])", new)));
                        }
                        *n = new;
                    }
                    _ => {}
                }
            }
        }

        Ok(self)
    }

    /// Adds a chapter, as a file name, to the book
    ///
    /// `Book` will then parse the file and store the AST (i.e., a vector
    /// of `Token`s).
    ///
    /// # Arguments
    /// * `number`: specifies if the chapter must be numbered, not numbered, or if its title
    ///   must be hidden. See `Number`.
    /// * `file`: path of the file for this chapter
    ///
    /// **Returns** an error if `file` does not exist, could not be read, of if there was
    /// some error parsing it.
    pub fn add_chapter(
        &mut self,
        number: Number,
        file: &str,
        add_title_if_empty: bool,
    ) -> Result<&mut Self> {
        self.bar_set_message(
            Crowbar::Main,
            &lformat!("Parsing {file}", file = misc::normalize(file)),
        );
        debug!(
            "{}",
            lformat!("Parsing chapter: {file}...", file = misc::normalize(file))
        );

        // try to open file
        let path = self.root.join(file);
        let f = File::open(&path).map_err(|_| {
            Error::file_not_found(
                &self.source,
                lformat!("book chapter"),
                format!("{}", path.display()),
            )
        })?;

        self.add_chapter_from_named_source(number, file, f, add_title_if_empty)
    }

    /// Adds a chapter to the book from a source (any object implementing `Read`)
    ///
    /// `Book` will then parse the string and store the AST (i.e., a vector
    /// of `Token`s).
    ///
    /// # Arguments
    /// * `number`: specifies if the chapter must be numbered, not numbered, or if its title
    ///   must be hidden. See `Number`.
    /// * `content`: the content of the chapter.
    ///
    /// **Returns** an error if there was some errror parsing `content`.
    pub fn add_chapter_from_source<R: Read>(
        &mut self,
        number: Number,
        source: R,
        add_title_if_empty: bool,
    ) -> Result<&mut Self> {
        self.add_chapter_from_named_source(number, "", source, add_title_if_empty)
    }

    /// Either clean a string or does nothing,
    /// according to book `lang` and `autoclean` options
    #[doc(hidden)]
    pub fn clean<'s, S: Into<Cow<'s, str>>>(&self, text: S) -> Cow<'s, str> {
        self.cleaner.clean(text.into())
    }

    /// Returns a template
    ///
    /// Returns the default one if no option was set, or the one set by the user.
    ///
    /// Returns an error if `template` isn't a valid template name.
    #[doc(hidden)]
    pub fn get_template(&self, template: &str) -> Result<Cow<'static, str>> {
        let option = self.options.get_path(template);
        let fallback = match template {
            "epub.css" => epub::CSS,
            "epub.chapter.xhtml" => {
                if self.options.get_i32("epub.version")? == 3 {
                    epub3::TEMPLATE
                } else {
                    epub::TEMPLATE
                }
            }
            "html.css" => html::CSS,
            "html.css.colors" => html::CSS_COLORS,
            "html.css.print" => html::PRINT_CSS,
            "html.standalone.template" => html_single::HTML,
            "html.standalone.js" => html_single::JS,
            "html.js" => html::JS,
            "html.dir.template" => html_dir::TEMPLATE,
            "html.highlight.js" => highlight::JS,
            "html.highlight.css" => highlight::CSS,
            "html.if.js" => html_if::JS,
            "html.if.new_game" => html_if::NEW_GAME,
            "tex.template" => latex::TEMPLATE,
            _ => {
                return Err(Error::config_parser(
                    &self.source,
                    lformat!("invalid template '{template}'", template = template),
                ))
            }
        };
        if let Ok(ref s) = option {
            let mut f = File::open(s).map_err(|_| {
                Error::file_not_found(
                    &self.source,
                    format!("template '{template}'", template = template),
                    s.to_owned(),
                )
            })?;
            let mut res = String::new();
            f.read_to_string(&mut res).map_err(|_| {
                Error::config_parser(
                    &self.source,
                    lformat!("file '{file}' could not be read", file = s),
                )
            })?;
            Ok(Cow::Owned(res))
        } else {
            Ok(Cow::Borrowed(fallback))
        }
    }

    /// Sets the chapter_template once and for all
    fn set_chapter_template(&mut self) -> Result<()> {
        let template = compile_str(
            self.options.get_str("rendering.chapter.template").unwrap(),
            &self.source,
            "rendering.chapter.template",
        )?;
        self.chapter_template = Some(template);
        Ok(())
    }

    /// Returns the formatted (roman or arabic) number of chapter
    #[doc(hidden)]
    pub fn get_header_number(&self, header: Header, n: i32) -> Result<String> {
        let boolean = match header {
            Header::Part => self
                .options
                .get_bool("rendering.part.roman_numerals")
                .unwrap(),
            Header::Chapter => self
                .options
                .get_bool("rendering.chapter.roman_numerals")
                .unwrap(),
        };
        let number = if boolean {
            if n <= 0 {
                return Err(Error::render(
                    Source::empty(),
                    lformat!(
                        "can not use roman numerals with zero or negative chapter numbers ({n})",
                        n = n
                    ),
                ));
            }
            format!("{:X}", Roman::from(n as i16))
        } else {
            format!("{}", n)
        };
        Ok(number)
    }

    /// Returns the string corresponding to a number, title, and the numbering template for chapter
    #[doc(hidden)]
    pub fn get_header<F>(
        &self,
        header: Header,
        n: i32,
        title: String,
        mut f: F,
    ) -> Result<HeaderData>
    where
        F: FnMut(&str) -> Result<String>,
    {
        let header_type = match header {
            Header::Part => "part",
            Header::Chapter => "chapter",
        };
        let mut data = self.get_metadata(&mut f)?;
        if !title.is_empty() {
            data = data.insert_bool(format!("has_{}_title", header_type), true);
        }
        let number = self.get_header_number(header, n)?;
        let header_name = self
            .options
            .get_str(&format!("rendering.{}", header_type))
            .map(|s| s.to_owned())
            .unwrap_or_else(|_| lang::get_str(self.options.get_str("lang").unwrap(), header_type));

        data = data
            .insert_str(format!("{}_title", header_type), title.clone())
            .insert_str(header_type, header_name.clone())
            .insert_str("number", number.clone());
        let data = data.build();
        let mut res: Vec<u8> = vec![];

        let opt_template = match header {
            Header::Part => &self.part_template,
            Header::Chapter => &self.chapter_template,
        };

        if let Some(ref template) = *opt_template {
            template.render_data(&mut res, &data)?;
        } else {
            let template = compile_str(
                self.options
                    .get_str(&format!("rendering.{}.template", header_type))
                    .unwrap(),
                &self.source,
                &format!("rendering.{}.template", header_type),
            )?;
            template.render_data(&mut res, &data)?;
        }

        match String::from_utf8(res) {
            Err(_) => panic!(
                "{}",
                lformat!("header generated by mustache was not valid utf-8")
            ),
            Ok(res) => Ok(HeaderData {
                text: res,
                number,
                header: header_name,
                title,
            }),
        }
    }

    /// Returns the string corresponding to a number, title, and the numbering template for chapter
    #[doc(hidden)]
    pub fn get_chapter_header<F>(&self, n: i32, title: String, f: F) -> Result<HeaderData>
    where
        F: FnMut(&str) -> Result<String>,
    {
        self.get_header(Header::Chapter, n, title, f)
    }

    /// Returns the string corresponding to a number, title, and the numbering template for part
    #[doc(hidden)]
    pub fn get_part_header<F>(&self, n: i32, title: String, f: F) -> Result<HeaderData>
    where
        F: FnMut(&str) -> Result<String>,
    {
        self.get_header(Header::Part, n, title, f)
    }

    /// Returns a `MapBuilder` (used by `Mustache` for templating), to be used (and completed)
    /// by renderers. It fills it with the metadata options.
    ///
    /// It also uses the lang/xx.yaml file corresponding to the language and fills
    /// `loc_xxx` fiels with it that corresponds to translated versions.
    ///
    /// This method treats the metadata as Markdown and thus calls `f` to render it.
    #[doc(hidden)]
    pub fn get_metadata<F>(&self, mut f: F) -> Result<MapBuilder>
    where
        F: FnMut(&str) -> Result<String>,
    {
        let mut mapbuilder = MapBuilder::new();
        mapbuilder = mapbuilder.insert_str("crowbook_version", env!("CARGO_PKG_VERSION"));
        mapbuilder = mapbuilder.insert_bool(
            format!("lang_{}", self.options.get_str("lang").unwrap()),
            true,
        );

        // Add metadata to mapbuilder
        for key in self.options.get_metadata() {
            if let Ok(s) = self.options.get_str(key) {
                let key = key.replace('.', "_");

                // Don't render lang as markdown
                let content = match key.as_ref() {
                    "lang" => Ok(s.to_string()),
                    _ => f(s),
                };
                let raw = view_as_text(&Parser::from(self).parse(s, None)?);
                match content {
                    Ok(content) => {
                        if !content.is_empty() {
                            mapbuilder = mapbuilder.insert_str(format!("{}_raw", key), raw);
                            mapbuilder = mapbuilder.insert_str(key.clone(), content);

                            mapbuilder = mapbuilder.insert_bool(format!("has_{}", key), true);
                        } else {
                            mapbuilder = mapbuilder.insert_bool(format!("has_{}", key), false);
                        }
                    }
                    Err(err) => {
                        return Err(Error::render(
                            &self.source,
                            lformat!(
                                "could not render `{key}` for \
                                                           metadata:\n{error}",
                                key = &key,
                                error = err
                            ),
                        ));
                    }
                }
            } else {
                mapbuilder = mapbuilder.insert_bool(format!("has_{}", key), false);
            }
        }

        // Add localization strings
        let hash = lang::get_hash(self.options.get_str("lang").unwrap());
        for (key, value) in hash {
            let key = format!("loc_{}", key.as_str().unwrap());
            let value = value.as_str().unwrap();
            mapbuilder = mapbuilder.insert_str(key, value);
        }
        Ok(mapbuilder)
    }

    /// Remove YAML blocks from a string and try to parse them to set options
    ///
    /// YAML blocks start with
    /// ---
    /// and end either with
    /// ---
    /// or
    /// ...
    fn parse_yaml(&mut self, yaml_block: &String) {
        // Checks that this is valid YAML
        match YamlLoader::load_from_str(yaml_block) {
            Ok(docs) => {
                // Use this yaml block to set options only if 1) it is valid
                // 2) the option is activated
                if docs.len() >= 1
                    && docs[0].as_hash().is_some()
                {
                    let hash = docs[0].as_hash().unwrap();
                    for (key, value) in hash {
                        match self
                            .options
                            //todo: remove clone
                            .set_yaml(key.clone(), value.clone())
                        {
                            Ok(opt) => {
                                if let Some(old_value) = opt {
                                    debug!(
                                        "{}",
                                        lformat!(
                                            "Inline YAML block \
                                            replaced {:?} \
                                            previously set to \
                                            {:?} to {:?}",
                                            key,
                                            old_value,
                                            value
                                        )
                                    );
                                } else {
                                    debug!(
                                        "{}",
                                        lformat!(
                                            "Inline YAML block \
                                            set {:?} to {:?}",
                                            key,
                                            value
                                        )
                                    );
                                }
                            },
                            Err(e) => {
                                error!("{}", lformat!("Inline YAML block could \
                                                        not set {:?} to {:?}: {}",
                                                        key,
                                                        value,
                                                        e))
                            }
                        }
                    }
                
                    self.update_cleaner();
                    self.init_checker();
                } else {
                    debug!(
                        "{}",
                        lformat!(
                            "Ignoring YAML \
                            block:
                            \n{block}",
                            block = &yaml_block
                        )
                    );
                }
            },
            Err(err) => {
                error!(
                    "{}",
                    lformat!(
                        "Found something that looked like a \
                            YAML block:\n{block}",
                        block = &yaml_block
                    )
                );
                error!(
                    "{}",
                    lformat!(
                        "... but it didn't parse correctly as \
                        YAML('{error}'), so treating it like \
                        Markdown.",
                        error = err
                    )
                );
            }
        }
    }

    // Update the cleaner according to autoclean and lang options
    fn update_cleaner(&mut self) {
        let params = CleanerParams {
            smart_quotes: self.options.get_bool("input.clean.smart_quotes").unwrap(),
            ligature_dashes: self
                .options
                .get_bool("input.clean.ligature.dashes")
                .unwrap(),
            ligature_guillemets: self
                .options
                .get_bool("input.clean.ligature.guillemets")
                .unwrap(),
        };
        if self.options.get_bool("input.clean").unwrap() {
            let lang = self.options.get_str("lang").unwrap().to_lowercase();
            let cleaner: Box<dyn Cleaner> = if lang.starts_with("fr") {
                Box::new(French::new(params))
            } else {
                Box::new(Default::new(params))
            };
            self.cleaner = cleaner;
        } else {
            self.cleaner = Box::new(Off);
        }
    }
}

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

/// Calls mustache::compile_str but catches panics and returns a result
pub fn compile_str<O>(template: &str, source: O, template_name: &str) -> Result<mustache::Template>
where
    O: Into<Source>,
{
    let input: String = template.to_owned();
    let result = mustache::compile_str(&input);
    match result {
        Ok(result) => Ok(result),
        Err(err) => Err(Error::template(
            source,
            lformat!(
                "could not compile '{template}': {error}",
                template = template_name,
                error = err
            ),
        )),
    }
}
