mdbook/src/renderer/html_handlebars/helpers/toc.rs

198 lines
6.4 KiB
Rust
Raw Normal View History

2018-07-23 12:45:01 -05:00
use std::path::Path;
use std::{cmp::Ordering, collections::BTreeMap};
use crate::utils;
use crate::utils::bracket_escape;
2018-08-05 15:08:47 +08:00
use handlebars::{Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError};
// Handlebars helper to construct TOC
#[derive(Clone, Copy)]
pub struct RenderToc {
pub no_section_label: bool,
}
impl HelperDef for RenderToc {
2018-08-21 10:58:44 -05:00
fn call<'reg: 'rc, 'rc>(
&self,
_h: &Helper<'reg, 'rc>,
2020-01-24 11:01:44 +08:00
_r: &'reg Handlebars<'_>,
ctx: &'rc Context,
2020-01-24 11:01:44 +08:00
rc: &mut RenderContext<'reg, 'rc>,
out: &mut dyn Output,
2018-08-21 10:58:44 -05:00
) -> 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
2019-07-13 00:11:05 +08:00
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(|_| RenderError::new("Could not decode the JSON data"))
})?;
let current_path = rc
2019-07-13 00:11:05 +08:00
.evaluate(ctx, "@root/path")?
.as_json()
.as_str()
2020-05-10 08:29:50 -07:00
.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()
2020-05-10 08:29:50 -07:00
.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()
2020-05-10 08:29:50 -07:00
.ok_or_else(|| RenderError::new("Type error for `fold_level`, u64 expected"))?;
2018-08-05 15:08:47 +08:00
out.write("<ol class=\"chapter\">")?;
let mut current_level = 1;
2017-06-13 20:53:25 +08:00
for item in chapters {
// Spacer
if item.get("spacer").is_some() {
2018-08-05 15:08:47 +08:00
out.write("<li class=\"spacer\"></li>")?;
continue;
}
let (section, level) = if let Some(s) = item.get("section") {
(s.as_str(), s.matches('.').count())
} else {
("", 1)
};
2020-05-10 08:29:50 -07:00
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
2020-05-10 08:29:50 -07:00
};
match level.cmp(&current_level) {
Ordering::Greater => {
while level > current_level {
out.write("<li>")?;
out.write("<ol class=\"section\">")?;
current_level += 1;
}
write_li_open_tag(out, is_expanded, false)?;
}
Ordering::Less => {
while level < current_level {
out.write("</ol>")?;
out.write("</li>")?;
current_level -= 1;
}
write_li_open_tag(out, is_expanded, false)?;
}
Ordering::Equal => {
write_li_open_tag(out, is_expanded, item.get("section").is_none())?;
}
}
2020-03-20 21:18:07 -05:00
// Part title
if let Some(title) = item.get("part") {
out.write("<li class=\"part-title\">")?;
out.write(&bracket_escape(title))?;
2020-03-20 21:18:07 -05:00
out.write("</li>")?;
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("<a href=\"")?;
let tmp = Path::new(item.get("path").expect("Error: path should be Some(_)"))
.with_extension("html")
.to_str()
.unwrap()
// Hack for windows who tends to use `\` as separator instead of `/`
.replace('\\', "/");
// Add link
out.write(&utils::fs::path_to_root(&current_path))?;
out.write(&tmp)?;
out.write("\"")?;
if path == &current_path {
out.write(" class=\"active\"")?;
}
out.write(">")?;
true
} else {
out.write("<div>")?;
false
};
if !self.no_section_label {
// Section does not necessarily exist
if let Some(section) = item.get("section") {
2018-08-05 15:08:47 +08:00
out.write("<strong aria-hidden=\"true\">")?;
out.write(section)?;
2018-08-05 15:08:47 +08:00
out.write("</strong> ")?;
}
}
if let Some(name) = item.get("name") {
out.write(&bracket_escape(name))?
}
if path_exists {
2018-08-05 15:08:47 +08:00
out.write("</a>")?;
} else {
out.write("</div>")?;
}
// 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 {
out.write("<a class=\"toggle\"><div>❱</div></a>")?;
}
}
2018-08-05 15:08:47 +08:00
out.write("</li>")?;
}
while current_level > 1 {
2018-08-05 15:08:47 +08:00
out.write("</ol>")?;
out.write("</li>")?;
current_level -= 1;
}
2018-08-05 15:08:47 +08:00
out.write("</ol>")?;
Ok(())
}
}
fn write_li_open_tag(
out: &mut dyn Output,
is_expanded: bool,
is_affix: bool,
) -> Result<(), std::io::Error> {
2020-04-03 12:09:23 -07:00
let mut li = String::from("<li class=\"chapter-item ");
if is_expanded {
li.push_str("expanded ");
}
if is_affix {
li.push_str("affix ");
}
li.push_str("\">");
out.write(&li)
}