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};
#[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> {
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())
})?;
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);
let is_expanded = !fold_enable || level - 1 < (fold_level as usize);
match level.cmp(¤t_level) {
Ordering::Greater => {
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;
if item.contains_key("spacer") {
out.write("<li class=\"spacer\"></li>")?;
continue;
}
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\">")?;
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();
out.write(&tmp)?;
out.write(if is_toc_html {
"\" target=\"_parent\">"
} else {
"\">"
})?;
true
}
_ => {
out.write("<span>")?;
false
}
};
if !self.no_section_label {
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>")?;
}
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 {
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)
}