use std::collections::BTreeMap; use std::io; use std::path::Path; use crate::utils; use handlebars::{Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError}; use pulldown_cmark::{html, Event, Parser}; // Handlebars helper to construct TOC #[derive(Clone, Copy)] pub struct RenderToc { pub no_section_label: bool, } impl HelperDef for RenderToc { fn call<'reg: 'rc, 'rc>( &self, _h: &Helper<'reg, '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::>>(c.as_json().clone()) .map_err(|_| RenderError::new("Could not decode the JSON data")) })?; let current_path = rc .evaluate(ctx, "@root/path")? .as_json() .as_str() .ok_or_else(|| RenderError::new("Type error for `path`, string expected"))? .replace("\"", ""); let current_section = rc .evaluate(ctx, "@root/section")? .as_json() .as_str() .map(str::to_owned) .unwrap_or_default(); let fold_enable = rc .evaluate(ctx, "@root/fold_enable")? .as_json() .as_bool() .ok_or_else(|| RenderError::new("Type error for `fold_enable`, bool expected"))?; let fold_level = rc .evaluate(ctx, "@root/fold_level")? .as_json() .as_u64() .ok_or_else(|| RenderError::new("Type error for `fold_level`, u64 expected"))?; out.write("
    ")?; let mut current_level = 1; for item in chapters { // Spacer if item.get("spacer").is_some() { out.write("
  1. ")?; continue; } let (section, level) = if let Some(s) = item.get("section") { (s.as_str(), s.matches('.').count()) } else { ("", 1) }; let is_expanded = if !fold_enable || (!section.is_empty() && current_section.starts_with(section)) { // Expand if folding is disabled, or if the section is an // ancestor or the current section itself. true } else { // Levels that are larger than this would be folded. level - 1 < fold_level as usize }; if level > current_level { while level > current_level { out.write("
  2. ")?; out.write("
      ")?; current_level += 1; } write_li_open_tag(out, is_expanded, false)?; } else if level < current_level { while level < current_level { out.write("
    ")?; out.write("
  3. ")?; current_level -= 1; } write_li_open_tag(out, is_expanded, false)?; } else { write_li_open_tag(out, is_expanded, item.get("section").is_none())?; } // Part title if let Some(title) = item.get("part") { out.write("
  4. ")?; write_escaped(out, title)?; out.write("
  5. ")?; continue; } // Link let path_exists = if let Some(path) = item.get("path") .and_then(|p| if p.is_empty() { None } else { Some(p) }) { out.write("")?; true } else { out.write("
    ")?; false }; if !self.no_section_label { // Section does not necessarily exist if let Some(section) = item.get("section") { out.write("")?; out.write(§ion)?; out.write(" ")?; } } if let Some(name) = item.get("name") { // Render only inline code blocks // filter all events that are not inline code blocks let parser = Parser::new(name).filter(|event| match *event { Event::Code(_) | Event::Html(_) | Event::Text(_) => true, _ => false, }); // render markdown to html let mut markdown_parsed_name = String::with_capacity(name.len() * 3 / 2); html::push_html(&mut markdown_parsed_name, parser); // write to the handlebars template write_escaped(out, &markdown_parsed_name)?; } if path_exists { out.write("")?; } else { out.write("
    ")?; } // Render expand/collapse toggle if let Some(flag) = item.get("has_sub_items") { let has_sub_items = flag.parse::().unwrap_or_default(); if fold_enable && has_sub_items { out.write("
    ")?; } } out.write("")?; } while current_level > 1 { out.write("
")?; out.write("")?; current_level -= 1; } out.write("")?; Ok(()) } } fn write_li_open_tag( out: &mut dyn Output, is_expanded: bool, is_affix: bool, ) -> Result<(), std::io::Error> { let mut li = String::from("
  • "); out.write(&li) } fn write_escaped(out: &mut dyn Output, mut title: &str) -> io::Result<()> { let needs_escape: &[char] = &['<', '>']; while let Some(next) = title.find(needs_escape) { out.write(&title[..next])?; match title.as_bytes()[next] { b'<' => out.write("<")?, b'>' => out.write(">")?, _ => unreachable!(), } title = &title[next + 1..]; } out.write(title)?; Ok(()) }