From 889c996e9ccdc316ad6c1db436c31bf2b4e01270 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Sun, 15 Sep 2019 22:13:36 +0800 Subject: [PATCH 1/7] render(toc): render expandable toc toggle --- src/renderer/html_handlebars/hbs_renderer.rs | 9 ++++ src/renderer/html_handlebars/helpers/toc.rs | 56 +++++++++++++++----- 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index 7a7f0ef6d7..aa63a4560b 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"); @@ -481,6 +485,11 @@ fn make_data( chapter.insert("section".to_owned(), json!(section.to_string())); } + chapter.insert( + "sub_items_count".to_owned(), + json!(ch.sub_items.len().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..e4d71880b9 100644 --- a/src/renderer/html_handlebars/helpers/toc.rs +++ b/src/renderer/html_handlebars/helpers/toc.rs @@ -28,13 +28,20 @@ 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(); + out.write("
    ")?; let mut current_level = 1; @@ -46,32 +53,30 @@ 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 = !section.is_empty() && current_section.starts_with(section); + if level > current_level { while level > current_level { out.write("
  1. ")?; out.write("
      ")?; current_level += 1; } - out.write("
    1. ")?; + write_li_open_tag(out, is_expanded, false)?; } else if level < current_level { while level < current_level { out.write("
    ")?; out.write("
  2. ")?; current_level -= 1; } - out.write("
  3. ")?; + 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 +92,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 +139,13 @@ impl HelperDef for RenderToc { out.write("")?; } + // Render expand/collapse toggle + if let Some(count) = item.get("sub_items_count") { + let count: u32 = count.parse().unwrap_or_default(); + if count > 0 { + out.write("
    ")?; + } + } out.write("
  4. ")?; } while current_level > 1 { @@ -146,3 +158,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("
  5. "); + out.write(&li) +} From 04b45db29b7c68957fb634418d49001235ac99dc Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Sun, 15 Sep 2019 22:13:50 +0800 Subject: [PATCH 2/7] ui(toc): js/css logic to toggle toc --- src/theme/book.js | 11 +++++++++++ src/theme/css/chrome.css | 30 +++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) 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; From 228b9ddbe8d68c9b2f7e9b2615d187874d2d8089 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Sun, 15 Sep 2019 23:00:05 +0800 Subject: [PATCH 3/7] test: update rendered output css selector --- tests/rendered_output.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) 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) From dce05df94e6e129d8820170b90f24ec99a3253c9 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Tue, 17 Sep 2019 20:30:56 +0800 Subject: [PATCH 4/7] config: add `html.fold.[enable|level]` --- src/config.rs | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/src/config.rs b/src/config.rs index 17c941d255..6665cf106d 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,27 @@ impl HtmlConfig { } } +/// Configuration for how to fold chapters of sidebar. +#[derive(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, +} + +impl Default for Fold { + fn default() -> Self { + Self { + enable: false, + level: 0, + } + } +} + /// Configuration for tweaking how the the HTML renderer handles the playpen. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(default, rename_all = "kebab-case")] From 22de1cf50f18d2919eecf44b4db1823547e4002e Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Tue, 17 Sep 2019 20:58:04 +0800 Subject: [PATCH 5/7] renderer: fold according to configs --- src/renderer/html_handlebars/hbs_renderer.rs | 8 +++-- src/renderer/html_handlebars/helpers/toc.rs | 35 +++++++++++++++++--- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index aa63a4560b..f29e095ef9 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -448,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(); @@ -467,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", @@ -486,8 +490,8 @@ fn make_data( } chapter.insert( - "sub_items_count".to_owned(), - json!(ch.sub_items.len().to_string()), + "has_sub_items".to_owned(), + json!((ch.sub_items.len() > 0).to_string()), ); chapter.insert("name".to_owned(), json!(ch.name)); diff --git a/src/renderer/html_handlebars/helpers/toc.rs b/src/renderer/html_handlebars/helpers/toc.rs index e4d71880b9..e5e34d3068 100644 --- a/src/renderer/html_handlebars/helpers/toc.rs +++ b/src/renderer/html_handlebars/helpers/toc.rs @@ -42,6 +42,22 @@ impl HelperDef for RenderToc { .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; @@ -59,7 +75,18 @@ impl HelperDef for RenderToc { ("", 1) }; - let is_expanded = !section.is_empty() && current_section.starts_with(section); + 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. + current_level - 1 < fold_level as usize + } + }; if level > current_level { while level > current_level { @@ -140,9 +167,9 @@ impl HelperDef for RenderToc { } // Render expand/collapse toggle - if let Some(count) = item.get("sub_items_count") { - let count: u32 = count.parse().unwrap_or_default(); - if count > 0 { + 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("
      ")?; } } From 450ad7fcf07585623b0b669a63e37129531b57dc Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Tue, 17 Sep 2019 21:31:43 +0800 Subject: [PATCH 6/7] doc: add `output.html.fold` --- book-example/src/format/config.md | 12 ++++++++++++ src/renderer/html_handlebars/helpers/toc.rs | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) 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/renderer/html_handlebars/helpers/toc.rs b/src/renderer/html_handlebars/helpers/toc.rs index e5e34d3068..c8075945e6 100644 --- a/src/renderer/html_handlebars/helpers/toc.rs +++ b/src/renderer/html_handlebars/helpers/toc.rs @@ -84,7 +84,7 @@ impl HelperDef for RenderToc { true } else { // Levels that are larger than this would be folded. - current_level - 1 < fold_level as usize + level - 1 < fold_level as usize } }; From 686d32586f93f7b1e04702442cc745172899bf3a Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Sun, 6 Oct 2019 07:37:42 +0800 Subject: [PATCH 7/7] refactor: tidy fold config - Derive default for `Fold`. - Use `is_empty` instead of checking the length of chapters. --- src/config.rs | 11 +---------- src/renderer/html_handlebars/hbs_renderer.rs | 2 +- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/config.rs b/src/config.rs index 6665cf106d..b99159ef19 100644 --- a/src/config.rs +++ b/src/config.rs @@ -466,7 +466,7 @@ impl HtmlConfig { } /// Configuration for how to fold chapters of sidebar. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(default, rename_all = "kebab-case")] pub struct Fold { /// When off, all folds are open. Default: `false`. @@ -477,15 +477,6 @@ pub struct Fold { pub level: u8, } -impl Default for Fold { - fn default() -> Self { - Self { - enable: false, - level: 0, - } - } -} - /// 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 f29e095ef9..8f8e78df50 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -491,7 +491,7 @@ fn make_data( chapter.insert( "has_sub_items".to_owned(), - json!((ch.sub_items.len() > 0).to_string()), + json!((!ch.sub_items.is_empty()).to_string()), ); chapter.insert("name".to_owned(), json!(ch.name));