mdbook-html 0.5.2

mdBook HTML renderer
Documentation
use crate::utils::ToUrlPath;
use handlebars::{
    Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError, RenderErrorReason,
};
use mdbook_core::utils::escape_html_attribute;
use std::path::Path;
use std::{cmp::Ordering, collections::BTreeMap};

// Handlebars helper to construct TOC
#[derive(Clone, Copy)]
pub(crate) struct RenderToc {
    pub no_section_label: bool,
}

impl HelperDef for RenderToc {
    fn call<'reg: 'rc, 'rc>(
        &self,
        _h: &Helper<'rc>,
        _r: &'reg Handlebars<'_>,
        ctx: &'rc Context,
        rc: &mut RenderContext<'reg, 'rc>,
        out: &mut dyn Output,
    ) -> Result<(), RenderError> {
        // get value from context data
        // rc.get_path() is current json parent path, you should always use it like this
        // param is the key of value you want to display
        let chapters = rc.evaluate(ctx, "@root/chapters").and_then(|c| {
            serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.as_json().clone())
                .map_err(|_| {
                    RenderErrorReason::Other("Could not decode the JSON data".to_owned()).into()
                })
        })?;

        let fold_enable = rc
            .evaluate(ctx, "@root/fold_enable")?
            .as_json()
            .as_bool()
            .ok_or_else(|| {
                RenderErrorReason::Other("Type error for `fold_enable`, bool expected".to_owned())
            })?;

        let fold_level = rc
            .evaluate(ctx, "@root/fold_level")?
            .as_json()
            .as_u64()
            .ok_or_else(|| {
                RenderErrorReason::Other("Type error for `fold_level`, u64 expected".to_owned())
            })?;

        // If true, then this is the iframe and we need target="_parent"
        let is_toc_html = rc
            .evaluate(ctx, "@root/is_toc_html")?
            .as_json()
            .as_bool()
            .unwrap_or(false);

        out.write("<ol class=\"chapter\">")?;

        let mut current_level = 1;
        let mut first = true;

        for item in chapters {
            let level = item
                .get("section")
                .map(|s| s.matches('.').count())
                .unwrap_or(1);

            // Expand if folding is disabled, or if levels that are larger than this would not
            // be folded.
            let is_expanded = !fold_enable || level - 1 < (fold_level as usize);

            match level.cmp(&current_level) {
                Ordering::Greater => {
                    // There is an assumption that when descending, it can
                    // only go one level down at a time. This should be
                    // enforced by the nature of markdown lists and the
                    // summary parser.
                    assert_eq!(level, current_level + 1);
                    current_level += 1;
                    out.write("<ol class=\"section\">")?;
                    write_li_open_tag(out, is_expanded)?;
                }
                Ordering::Less => {
                    while level < current_level {
                        out.write("</li>")?;
                        out.write("</ol>")?;
                        current_level -= 1;
                    }
                    write_li_open_tag(out, is_expanded)?;
                }
                Ordering::Equal => {
                    if !first {
                        out.write("</li>")?;
                    }
                    write_li_open_tag(out, is_expanded)?;
                }
            }
            first = false;

            // Spacer
            if item.contains_key("spacer") {
                out.write("<li class=\"spacer\"></li>")?;
                continue;
            }

            // Part title
            if let Some(title) = item.get("part") {
                out.write("<li class=\"part-title\">")?;
                out.write(&escape_html_attribute(title))?;
                out.write("</li>")?;
                continue;
            }

            out.write("<span class=\"chapter-link-wrapper\">")?;

            // Link
            let path_exists = match item.get("path") {
                Some(path) if !path.is_empty() => {
                    out.write("<a href=\"")?;
                    let tmp = Path::new(path).with_extension("html").to_url_path();

                    // Add link
                    out.write(&tmp)?;
                    out.write(if is_toc_html {
                        "\" target=\"_parent\">"
                    } else {
                        "\">"
                    })?;
                    true
                }
                _ => {
                    out.write("<span>")?;
                    false
                }
            };

            if !self.no_section_label {
                // Section does not necessarily exist
                if let Some(section) = item.get("section") {
                    out.write("<strong aria-hidden=\"true\">")?;
                    out.write(section)?;
                    out.write("</strong> ")?;
                }
            }

            if let Some(name) = item.get("name") {
                out.write(&escape_html_attribute(name))?;
            }

            if path_exists {
                out.write("</a>")?;
            } else {
                out.write("</span>")?;
            }

            // Render expand/collapse toggle
            if let Some(flag) = item.get("has_sub_items") {
                let has_sub_items = flag.parse::<bool>().unwrap_or_default();
                if fold_enable && has_sub_items {
                    // The <div> here is to manage rotating the element when
                    // the chapter title is long and word-wraps.
                    out.write("<a class=\"chapter-fold-toggle\"><div>❱</div></a>")?;
                }
            }
            out.write("</span>")?;
        }
        while current_level > 0 {
            out.write("</li>")?;
            out.write("</ol>")?;
            current_level -= 1;
        }

        Ok(())
    }
}

fn write_li_open_tag(out: &mut dyn Output, is_expanded: bool) -> Result<(), std::io::Error> {
    let mut li = String::from("<li class=\"chapter-item ");
    if is_expanded {
        li.push_str("expanded ");
    }
    li.push_str("\">");
    out.write(&li)
}