diff --git a/book-example/src/format/config.md b/book-example/src/format/config.md
index d443911f88..dab6c3fe79 100644
--- a/book-example/src/format/config.md
+++ b/book-example/src/format/config.md
@@ -166,6 +166,7 @@ The following configuration options are available:
- **no-section-label:** mdBook by defaults adds section label in table of
contents column. For example, "1.", "2.1". Set this option to true to disable
those labels. Defaults to `false`.
+- **fold:** A subtable for configuring sidebar section-folding behavior.
- **playpen:** A subtable for configuring various playpen settings.
- **search:** A subtable for configuring the in-browser search functionality.
mdBook must be compiled with the `search` feature enabled (on by default).
@@ -173,6 +174,13 @@ The following configuration options are available:
an icon link will be output in the menu bar of the book.
- **git-repository-icon:** The FontAwesome icon class to use for the git
repository link. Defaults to `fa-github`.
+
+Available configuration options for the `[output.html.fold]` table:
+
+- **enable:** Enable section-folding. When off, all folds are open.
+ Defaults to `false`.
+- **level:** The higher the more folded regions are open. When level is 0, all
+ folds are closed. Defaults to `0`.
Available configuration options for the `[output.html.playpen]` table:
@@ -225,6 +233,10 @@ no-section-label = false
git-repository-url = "https://github.com/rust-lang-nursery/mdBook"
git-repository-icon = "fa-github"
+[output.html.fold]
+enable = false
+level = 0
+
[output.html.playpen]
editable = false
copy-js = true
diff --git a/src/config.rs b/src/config.rs
index 17c941d255..b99159ef19 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -431,16 +431,10 @@ pub struct HtmlConfig {
/// Additional JS scripts to include at the bottom of the rendered page's
/// `
`.
pub additional_js: Vec,
+ /// Fold settings.
+ pub fold: Fold,
/// Playpen settings.
pub playpen: Playpen,
- /// This is used as a bit of a workaround for the `mdbook serve` command.
- /// Basically, because you set the websocket port from the command line, the
- /// `mdbook serve` command needs a way to let the HTML renderer know where
- /// to point livereloading at, if it has been enabled.
- ///
- /// This config item *should not be edited* by the end user.
- #[doc(hidden)]
- pub livereload_url: Option,
/// Don't render section labels.
pub no_section_label: bool,
/// Search settings. If `None`, the default will be used.
@@ -450,6 +444,14 @@ pub struct HtmlConfig {
/// FontAwesome icon class to use for the Git repository link.
/// Defaults to `fa-github` if `None`.
pub git_repository_icon: Option,
+ /// This is used as a bit of a workaround for the `mdbook serve` command.
+ /// Basically, because you set the websocket port from the command line, the
+ /// `mdbook serve` command needs a way to let the HTML renderer know where
+ /// to point livereloading at, if it has been enabled.
+ ///
+ /// This config item *should not be edited* by the end user.
+ #[doc(hidden)]
+ pub livereload_url: Option,
}
impl HtmlConfig {
@@ -463,6 +465,18 @@ impl HtmlConfig {
}
}
+/// Configuration for how to fold chapters of sidebar.
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(default, rename_all = "kebab-case")]
+pub struct Fold {
+ /// When off, all folds are open. Default: `false`.
+ pub enable: bool,
+ /// The higher the more folded regions are open. When level is 0, all folds
+ /// are closed.
+ /// Default: `0`.
+ pub level: u8,
+}
+
/// Configuration for tweaking how the the HTML renderer handles the playpen.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs
index 7a7f0ef6d7..8f8e78df50 100644
--- a/src/renderer/html_handlebars/hbs_renderer.rs
+++ b/src/renderer/html_handlebars/hbs_renderer.rs
@@ -71,6 +71,10 @@ impl HtmlHandlebars {
"path_to_root".to_owned(),
json!(utils::fs::path_to_root(&ch.path)),
);
+ if let Some(ref section) = ch.number {
+ ctx.data
+ .insert("section".to_owned(), json!(section.to_string()));
+ }
// Render the handlebars template with the data
debug!("Render template");
@@ -444,6 +448,9 @@ fn make_data(
data.insert("playpen_js".to_owned(), json!(true));
}
+ data.insert("fold_enable".to_owned(), json!((html_config.fold.enable)));
+ data.insert("fold_level".to_owned(), json!((html_config.fold.level)));
+
let search = html_config.search.clone();
if cfg!(feature = "search") {
let search = search.unwrap_or_default();
@@ -463,6 +470,7 @@ fn make_data(
if let Some(ref git_repository_url) = html_config.git_repository_url {
data.insert("git_repository_url".to_owned(), json!(git_repository_url));
}
+
let git_repository_icon = match html_config.git_repository_icon {
Some(ref git_repository_icon) => git_repository_icon,
None => "fa-github",
@@ -481,6 +489,11 @@ fn make_data(
chapter.insert("section".to_owned(), json!(section.to_string()));
}
+ chapter.insert(
+ "has_sub_items".to_owned(),
+ json!((!ch.sub_items.is_empty()).to_string()),
+ );
+
chapter.insert("name".to_owned(), json!(ch.name));
let path = ch
.path
diff --git a/src/renderer/html_handlebars/helpers/toc.rs b/src/renderer/html_handlebars/helpers/toc.rs
index 33e7ef843a..c8075945e6 100644
--- a/src/renderer/html_handlebars/helpers/toc.rs
+++ b/src/renderer/html_handlebars/helpers/toc.rs
@@ -28,13 +28,36 @@ impl HelperDef for RenderToc {
serde_json::value::from_value::>>(c.as_json().clone())
.map_err(|_| RenderError::new("Could not decode the JSON data"))
})?;
- let current = rc
+ let current_path = rc
.evaluate(ctx, "@root/path")?
.as_json()
.as_str()
- .ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
+ .ok_or(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(RenderError::new(
+ "Type error for `fold_enable`, bool expected",
+ ))?;
+
+ let fold_level = rc
+ .evaluate(ctx, "@root/fold_level")?
+ .as_json()
+ .as_u64()
+ .ok_or(RenderError::new(
+ "Type error for `fold_level`, u64 expected",
+ ))?;
+
out.write("")?;
let mut current_level = 1;
@@ -46,10 +69,23 @@ impl HelperDef for RenderToc {
continue;
}
- let level = if let Some(s) = item.get("section") {
- s.matches('.').count()
+ let (section, level) = if let Some(s) = item.get("section") {
+ (s.as_str(), s.matches('.').count())
} else {
- 1
+ ("", 1)
+ };
+
+ let is_expanded = {
+ if !fold_enable {
+ // Disable fold. Expand all chapters.
+ true
+ } else if !section.is_empty() && current_section.starts_with(section) {
+ // The section is 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 {
@@ -58,20 +94,16 @@ impl HelperDef for RenderToc {
out.write("")?;
current_level += 1;
}
- out.write("- ")?;
+ write_li_open_tag(out, is_expanded, false)?;
} else if level < current_level {
while level < current_level {
out.write("
")?;
out.write("")?;
current_level -= 1;
}
- out.write("- ")?;
+ write_li_open_tag(out, is_expanded, false)?;
} else {
- out.write("
- ")?;
+ write_li_open_tag(out, is_expanded, item.get("section").is_none())?;
}
// Link
@@ -87,11 +119,11 @@ impl HelperDef for RenderToc {
.replace("\\", "/");
// Add link
- out.write(&utils::fs::path_to_root(¤t))?;
+ out.write(&utils::fs::path_to_root(¤t_path))?;
out.write(&tmp)?;
out.write("\"")?;
- if path == ¤t {
+ if path == ¤t_path {
out.write(" class=\"active\"")?;
}
@@ -134,6 +166,13 @@ impl HelperDef for RenderToc {
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 {
@@ -146,3 +185,19 @@ impl HelperDef for RenderToc {
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)
+}
diff --git a/src/theme/book.js b/src/theme/book.js
index ca73ee14d0..8a5451ed3a 100644
--- a/src/theme/book.js
+++ b/src/theme/book.js
@@ -451,6 +451,17 @@ function playpen_text(playpen) {
try { localStorage.setItem('mdbook-sidebar', 'visible'); } catch (e) { }
}
+
+ var sidebarAnchorToggles = document.querySelectorAll('#sidebar a.toggle');
+
+ function toggleSection(ev) {
+ ev.currentTarget.parentElement.classList.toggle('expanded');
+ }
+
+ Array.from(sidebarAnchorToggles).forEach(function (el) {
+ el.addEventListener('click', toggleSection);
+ });
+
function hideSidebar() {
html.classList.remove('sidebar-visible')
html.classList.add('sidebar-hidden');
diff --git a/src/theme/css/chrome.css b/src/theme/css/chrome.css
index 495f9b6a32..35d76d0dd0 100644
--- a/src/theme/css/chrome.css
+++ b/src/theme/css/chrome.css
@@ -376,7 +376,13 @@ ul#searchresults span.teaser em {
padding-left: 0;
line-height: 2.2em;
}
+
+.chapter ol {
+ width: 100%;
+}
+
.chapter li {
+ display: flex;
color: var(--sidebar-non-existant);
}
.chapter li a {
@@ -390,10 +396,32 @@ ul#searchresults span.teaser em {
color: var(--sidebar-active);
}
-.chapter li .active {
+.chapter li a.active {
color: var(--sidebar-active);
}
+.chapter li > a.toggle {
+ cursor: pointer;
+ display: block;
+ margin-left: auto;
+ padding: 0 10px;
+ user-select: none;
+ opacity: 0.68;
+}
+
+.chapter li > a.toggle div {
+ transition: transform 0.5s;
+}
+
+/* collapse the section */
+.chapter li:not(.expanded) + li > ol {
+ display: none;
+}
+
+.chapter li.expanded > a.toggle div {
+ transform: rotate(90deg);
+}
+
.spacer {
width: 100%;
height: 3px;
diff --git a/tests/rendered_output.rs b/tests/rendered_output.rs
index 51bf4154fe..1da04389fc 100644
--- a/tests/rendered_output.rs
+++ b/tests/rendered_output.rs
@@ -235,7 +235,12 @@ fn check_second_toc_level() {
let mut should_be = Vec::from(TOC_SECOND_LEVEL);
should_be.sort();
- let pred = descendants!(Class("chapter"), Name("li"), Name("li"), Name("a"));
+ let pred = descendants!(
+ Class("chapter"),
+ Name("li"),
+ Name("li"),
+ Name("a").and(Class("toggle").not())
+ );
let mut children_of_children: Vec<_> = doc
.find(pred)
@@ -254,7 +259,11 @@ fn check_first_toc_level() {
should_be.extend(TOC_SECOND_LEVEL);
should_be.sort();
- let pred = descendants!(Class("chapter"), Name("li"), Name("a"));
+ let pred = descendants!(
+ Class("chapter"),
+ Name("li"),
+ Name("a").and(Class("toggle").not())
+ );
let mut children: Vec<_> = doc
.find(pred)