Skip to content

Commit

Permalink
Make the sidebar work without JS
Browse files Browse the repository at this point in the history
Uses an iframe instead. The downside of iframes comes from them
not necessarily being same-origin as the main page (particularly
with `file:///` URLs), which can cause themes to fall out of sync,
but that's not a problem here since themes don't work without JS
anyway.
  • Loading branch information
notriddle committed Jul 16, 2024
1 parent 2cb5b85 commit 203685e
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 14 deletions.
13 changes: 10 additions & 3 deletions src/renderer/html_handlebars/hbs_renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,9 @@ impl Renderer for HtmlHandlebars {
handlebars.register_partial("header", String::from_utf8(theme.header.clone())?)?;

debug!("Register the toc handlebars template");
handlebars.register_template_string("toc", String::from_utf8(theme.toc.clone())?)?;
handlebars.register_template_string("toc_js", String::from_utf8(theme.toc_js.clone())?)?;
handlebars
.register_template_string("toc_html", String::from_utf8(theme.toc_html.clone())?)?;

debug!("Register handlebars helpers");
self.register_hbs_helpers(&mut handlebars, &html_config);
Expand Down Expand Up @@ -586,11 +588,16 @@ impl Renderer for HtmlHandlebars {
debug!("Creating print.html ✓");
}

debug!("Render toc.js");
debug!("Render toc");
{
let rendered_toc = handlebars.render("toc", &data)?;
let rendered_toc = handlebars.render("toc_js", &data)?;
utils::fs::write_file(destination, "toc.js", rendered_toc.as_bytes())?;
debug!("Creating toc.js ✓");
data.insert("is_toc_html".to_owned(), json!(true));
let rendered_toc = handlebars.render("toc_html", &data)?;
utils::fs::write_file(destination, "toc.html", rendered_toc.as_bytes())?;
debug!("Creating toc.html ✓");
data.remove("is_toc_html");
}

debug!("Copy static files");
Expand Down
13 changes: 12 additions & 1 deletion src/renderer/html_handlebars/helpers/toc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ impl HelperDef for RenderToc {
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;
Expand Down Expand Up @@ -113,7 +120,11 @@ impl HelperDef for RenderToc {

// Add link
out.write(&tmp)?;
out.write("\">")?;
out.write(if is_toc_html {
"\" target=\"_parent\">"
} else {
"\">"
})?;
path_exists = true;
}
_ => {
Expand Down
16 changes: 16 additions & 0 deletions src/theme/css/chrome.css
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,22 @@ ul#searchresults span.teaser em {
background-color: var(--sidebar-bg);
color: var(--sidebar-fg);
}
.sidebar-iframe-inner {
background-color: var(--sidebar-bg);
color: var(--sidebar-fg);
padding: 10px 10px;
margin: 0;
font-size: 1.4rem;
}
.sidebar-iframe-outer {
border: none;
height: 100%;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
[dir=rtl] .sidebar { left: unset; right: 0; }
.sidebar-resizing {
-moz-user-select: none;
Expand Down
3 changes: 3 additions & 0 deletions src/theme/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
<!-- populated by js -->
<div class="sidebar-scrollbox"></div>
<noscript>
<iframe class="sidebar-iframe-outer" src="{{ path_to_root }}toc.html"></iframe>
</noscript>
<div id="sidebar-resize-handle" class="sidebar-resize-handle">
<div class="sidebar-resize-indicator"></div>
</div>
Expand Down
16 changes: 11 additions & 5 deletions src/theme/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ pub static INDEX: &[u8] = include_bytes!("index.hbs");
pub static HEAD: &[u8] = include_bytes!("head.hbs");
pub static REDIRECT: &[u8] = include_bytes!("redirect.hbs");
pub static HEADER: &[u8] = include_bytes!("header.hbs");
pub static TOC: &[u8] = include_bytes!("toc.js.hbs");
pub static TOC_JS: &[u8] = include_bytes!("toc.js.hbs");
pub static TOC_HTML: &[u8] = include_bytes!("toc.html.hbs");
pub static CHROME_CSS: &[u8] = include_bytes!("css/chrome.css");
pub static GENERAL_CSS: &[u8] = include_bytes!("css/general.css");
pub static PRINT_CSS: &[u8] = include_bytes!("css/print.css");
Expand Down Expand Up @@ -51,7 +52,8 @@ pub struct Theme {
pub head: Vec<u8>,
pub redirect: Vec<u8>,
pub header: Vec<u8>,
pub toc: Vec<u8>,
pub toc_js: Vec<u8>,
pub toc_html: Vec<u8>,
pub chrome_css: Vec<u8>,
pub general_css: Vec<u8>,
pub print_css: Vec<u8>,
Expand Down Expand Up @@ -87,7 +89,8 @@ impl Theme {
(theme_dir.join("head.hbs"), &mut theme.head),
(theme_dir.join("redirect.hbs"), &mut theme.redirect),
(theme_dir.join("header.hbs"), &mut theme.header),
(theme_dir.join("toc.js.hbs"), &mut theme.toc),
(theme_dir.join("toc.js.hbs"), &mut theme.toc_js),
(theme_dir.join("toc.html.hbs"), &mut theme.toc_html),
(theme_dir.join("book.js"), &mut theme.js),
(theme_dir.join("css/chrome.css"), &mut theme.chrome_css),
(theme_dir.join("css/general.css"), &mut theme.general_css),
Expand Down Expand Up @@ -177,7 +180,8 @@ impl Default for Theme {
head: HEAD.to_owned(),
redirect: REDIRECT.to_owned(),
header: HEADER.to_owned(),
toc: TOC.to_owned(),
toc_js: TOC_JS.to_owned(),
toc_html: TOC_HTML.to_owned(),
chrome_css: CHROME_CSS.to_owned(),
general_css: GENERAL_CSS.to_owned(),
print_css: PRINT_CSS.to_owned(),
Expand Down Expand Up @@ -237,6 +241,7 @@ mod tests {
"redirect.hbs",
"header.hbs",
"toc.js.hbs",
"toc.html.hbs",
"favicon.png",
"favicon.svg",
"css/chrome.css",
Expand Down Expand Up @@ -268,7 +273,8 @@ mod tests {
head: Vec::new(),
redirect: Vec::new(),
header: Vec::new(),
toc: Vec::new(),
toc_js: Vec::new(),
toc_html: Vec::new(),
chrome_css: Vec::new(),
general_css: Vec::new(),
print_css: Vec::new(),
Expand Down
43 changes: 43 additions & 0 deletions src/theme/toc.html.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<!DOCTYPE HTML>
<html lang="{{ language }}" class="{{ default_theme }}" dir="{{ text_direction }}">
<head>
<!-- sidebar iframe generated using mdBook
This is a frame, and not included directly in the page, to control the total size of the
book. The TOC contains an entry for each page, so if each page includes a copy of the TOC,
the total size of the page becomes O(n**2).
The frame is only used as a fallback when JS is turned off. When it's on, the sidebar is
instead added to the main page by `toc.js` instead. The JavaScript mode is better
because, when running in a `file:///` URL, the iframed page would not be Same-Origin as
the rest of the page, so the sidebar and the main page theme would fall out of sync.
-->
<meta charset="UTF-8">
<meta name="robots" content="noindex">
{{#if base_url}}
<base href="{{ base_url }}">
{{/if}}
<!-- Custom HTML head -->
{{> head}}
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff">
<link rel="stylesheet" href="{{ path_to_root }}css/variables.css">
<link rel="stylesheet" href="{{ path_to_root }}css/general.css">
<link rel="stylesheet" href="{{ path_to_root }}css/chrome.css">
{{#if print_enable}}
<link rel="stylesheet" href="{{ path_to_root }}css/print.css" media="print">
{{/if}}
<!-- Fonts -->
<link rel="stylesheet" href="{{ path_to_root }}FontAwesome/css/font-awesome.css">
{{#if copy_fonts}}
<link rel="stylesheet" href="{{ path_to_root }}fonts/fonts.css">
{{/if}}
<!-- Custom theme stylesheets -->
{{#each additional_css}}
<link rel="stylesheet" href="{{ ../path_to_root }}{{ this }}">
{{/each}}
</head>
<body class="sidebar-iframe-inner">
{{#toc}}{{/toc}}
</body>
</html>
58 changes: 53 additions & 5 deletions tests/rendered_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use mdbook::utils::fs::write_file;
use mdbook::MDBook;
use pretty_assertions::assert_eq;
use select::document::Document;
use select::predicate::{Class, Name, Predicate};
use select::predicate::{Attr, Class, Name, Predicate};
use std::collections::HashMap;
use std::ffi::OsStr;
use std::fs;
Expand Down Expand Up @@ -232,7 +232,7 @@ fn entry_ends_with(entry: &DirEntry, ending: &str) -> bool {

/// Read the TOC (`book/toc.js`) nested HTML and expose it as a DOM which we
/// can search with the `select` crate
fn toc_html() -> Result<Document> {
fn toc_js_html() -> Result<Document> {
let temp = DummyBook::new()
.build()
.with_context(|| "Couldn't create the dummy book")?;
Expand All @@ -252,9 +252,24 @@ fn toc_html() -> Result<Document> {
panic!("cannot find toc in file")
}

/// Read the TOC fallback (`book/toc.html`) HTML and expose it as a DOM which we
/// can search with the `select` crate
fn toc_fallback_html() -> Result<Document> {
let temp = DummyBook::new()
.build()
.with_context(|| "Couldn't create the dummy book")?;
MDBook::load(temp.path())?
.build()
.with_context(|| "Book building failed")?;

let toc_path = temp.path().join("book").join("toc.html");
let html = fs::read_to_string(toc_path).with_context(|| "Unable to read index.html")?;
Ok(Document::from(html.as_str()))
}

#[test]
fn check_second_toc_level() {
let doc = toc_html().unwrap();
let doc = toc_js_html().unwrap();
let mut should_be = Vec::from(TOC_SECOND_LEVEL);
should_be.sort_unstable();

Expand All @@ -276,7 +291,7 @@ fn check_second_toc_level() {

#[test]
fn check_first_toc_level() {
let doc = toc_html().unwrap();
let doc = toc_js_html().unwrap();
let mut should_be = Vec::from(TOC_TOP_LEVEL);

should_be.extend(TOC_SECOND_LEVEL);
Expand All @@ -299,7 +314,7 @@ fn check_first_toc_level() {

#[test]
fn check_spacers() {
let doc = toc_html().unwrap();
let doc = toc_js_html().unwrap();
let should_be = 2;

let num_spacers = doc
Expand All @@ -308,6 +323,39 @@ fn check_spacers() {
assert_eq!(num_spacers, should_be);
}

// don't use target="_parent" in JS
#[test]
fn check_link_target_js() {
let doc = toc_js_html().unwrap();

let num_parent_links = doc
.find(
Class("chapter")
.descendant(Name("li"))
.descendant(Name("a").and(Attr("target", "_parent"))),
)
.count();
assert_eq!(num_parent_links, 0);
}

// don't use target="_parent" in IFRAME
#[test]
fn check_link_target_fallback() {
let doc = toc_fallback_html().unwrap();

let num_parent_links = doc
.find(
Class("chapter")
.descendant(Name("li"))
.descendant(Name("a").and(Attr("target", "_parent"))),
)
.count();
assert_eq!(
num_parent_links,
TOC_TOP_LEVEL.len() + TOC_SECOND_LEVEL.len()
);
}

/// Ensure building fails if `create-missing` is false and one of the files does
/// not exist.
#[test]
Expand Down

0 comments on commit 203685e

Please sign in to comment.