This fixes an issue when folding is enabled. The folding was not properly hiding the sub-chapters because it was assuming it could hide the next list element. However, the heading nav was the next list element, so the remaining chapters remained visible. The solution required some deeper changes to how the chapters were organized in the sidebar. Instead of nested chapters being a list element *sibling*, the nested chapter's `ol` is now a *child* of its parent chapter. This makes it much easier to just hide everything without regard of the exact sibling order. This required wrapping the chapter title and the toggle chevron inside a span so that the flex layout could be localized to just those elements, and allow the following `ol` elements to lay out regularly. Closes https://github.com/rust-lang/mdBook/issues/2880
184 lines
6.2 KiB
Rust
184 lines
6.2 KiB
Rust
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(¤t_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)
|
|
}
|