Skip to content

Commit 203685e

Browse files
committed
Make the sidebar work without JS
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.
1 parent 2cb5b85 commit 203685e

File tree

7 files changed

+148
-14
lines changed

7 files changed

+148
-14
lines changed

src/renderer/html_handlebars/hbs_renderer.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -529,7 +529,9 @@ impl Renderer for HtmlHandlebars {
529529
handlebars.register_partial("header", String::from_utf8(theme.header.clone())?)?;
530530

531531
debug!("Register the toc handlebars template");
532-
handlebars.register_template_string("toc", String::from_utf8(theme.toc.clone())?)?;
532+
handlebars.register_template_string("toc_js", String::from_utf8(theme.toc_js.clone())?)?;
533+
handlebars
534+
.register_template_string("toc_html", String::from_utf8(theme.toc_html.clone())?)?;
533535

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

589-
debug!("Render toc.js");
591+
debug!("Render toc");
590592
{
591-
let rendered_toc = handlebars.render("toc", &data)?;
593+
let rendered_toc = handlebars.render("toc_js", &data)?;
592594
utils::fs::write_file(destination, "toc.js", rendered_toc.as_bytes())?;
593595
debug!("Creating toc.js ✓");
596+
data.insert("is_toc_html".to_owned(), json!(true));
597+
let rendered_toc = handlebars.render("toc_html", &data)?;
598+
utils::fs::write_file(destination, "toc.html", rendered_toc.as_bytes())?;
599+
debug!("Creating toc.html ✓");
600+
data.remove("is_toc_html");
594601
}
595602

596603
debug!("Copy static files");

src/renderer/html_handlebars/helpers/toc.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ impl HelperDef for RenderToc {
4848
RenderErrorReason::Other("Type error for `fold_level`, u64 expected".to_owned())
4949
})?;
5050

51+
// If true, then this is the iframe and we need target="_parent"
52+
let is_toc_html = rc
53+
.evaluate(ctx, "@root/is_toc_html")?
54+
.as_json()
55+
.as_bool()
56+
.unwrap_or(false);
57+
5158
out.write("<ol class=\"chapter\">")?;
5259

5360
let mut current_level = 1;
@@ -113,7 +120,11 @@ impl HelperDef for RenderToc {
113120

114121
// Add link
115122
out.write(&tmp)?;
116-
out.write("\">")?;
123+
out.write(if is_toc_html {
124+
"\" target=\"_parent\">"
125+
} else {
126+
"\">"
127+
})?;
117128
path_exists = true;
118129
}
119130
_ => {

src/theme/css/chrome.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,22 @@ ul#searchresults span.teaser em {
399399
background-color: var(--sidebar-bg);
400400
color: var(--sidebar-fg);
401401
}
402+
.sidebar-iframe-inner {
403+
background-color: var(--sidebar-bg);
404+
color: var(--sidebar-fg);
405+
padding: 10px 10px;
406+
margin: 0;
407+
font-size: 1.4rem;
408+
}
409+
.sidebar-iframe-outer {
410+
border: none;
411+
height: 100%;
412+
position: absolute;
413+
top: 0;
414+
bottom: 0;
415+
left: 0;
416+
right: 0;
417+
}
402418
[dir=rtl] .sidebar { left: unset; right: 0; }
403419
.sidebar-resizing {
404420
-moz-user-select: none;

src/theme/index.hbs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@
111111
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
112112
<!-- populated by js -->
113113
<div class="sidebar-scrollbox"></div>
114+
<noscript>
115+
<iframe class="sidebar-iframe-outer" src="{{ path_to_root }}toc.html"></iframe>
116+
</noscript>
114117
<div id="sidebar-resize-handle" class="sidebar-resize-handle">
115118
<div class="sidebar-resize-indicator"></div>
116119
</div>

src/theme/mod.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ pub static INDEX: &[u8] = include_bytes!("index.hbs");
1717
pub static HEAD: &[u8] = include_bytes!("head.hbs");
1818
pub static REDIRECT: &[u8] = include_bytes!("redirect.hbs");
1919
pub static HEADER: &[u8] = include_bytes!("header.hbs");
20-
pub static TOC: &[u8] = include_bytes!("toc.js.hbs");
20+
pub static TOC_JS: &[u8] = include_bytes!("toc.js.hbs");
21+
pub static TOC_HTML: &[u8] = include_bytes!("toc.html.hbs");
2122
pub static CHROME_CSS: &[u8] = include_bytes!("css/chrome.css");
2223
pub static GENERAL_CSS: &[u8] = include_bytes!("css/general.css");
2324
pub static PRINT_CSS: &[u8] = include_bytes!("css/print.css");
@@ -51,7 +52,8 @@ pub struct Theme {
5152
pub head: Vec<u8>,
5253
pub redirect: Vec<u8>,
5354
pub header: Vec<u8>,
54-
pub toc: Vec<u8>,
55+
pub toc_js: Vec<u8>,
56+
pub toc_html: Vec<u8>,
5557
pub chrome_css: Vec<u8>,
5658
pub general_css: Vec<u8>,
5759
pub print_css: Vec<u8>,
@@ -87,7 +89,8 @@ impl Theme {
8789
(theme_dir.join("head.hbs"), &mut theme.head),
8890
(theme_dir.join("redirect.hbs"), &mut theme.redirect),
8991
(theme_dir.join("header.hbs"), &mut theme.header),
90-
(theme_dir.join("toc.js.hbs"), &mut theme.toc),
92+
(theme_dir.join("toc.js.hbs"), &mut theme.toc_js),
93+
(theme_dir.join("toc.html.hbs"), &mut theme.toc_html),
9194
(theme_dir.join("book.js"), &mut theme.js),
9295
(theme_dir.join("css/chrome.css"), &mut theme.chrome_css),
9396
(theme_dir.join("css/general.css"), &mut theme.general_css),
@@ -177,7 +180,8 @@ impl Default for Theme {
177180
head: HEAD.to_owned(),
178181
redirect: REDIRECT.to_owned(),
179182
header: HEADER.to_owned(),
180-
toc: TOC.to_owned(),
183+
toc_js: TOC_JS.to_owned(),
184+
toc_html: TOC_HTML.to_owned(),
181185
chrome_css: CHROME_CSS.to_owned(),
182186
general_css: GENERAL_CSS.to_owned(),
183187
print_css: PRINT_CSS.to_owned(),
@@ -237,6 +241,7 @@ mod tests {
237241
"redirect.hbs",
238242
"header.hbs",
239243
"toc.js.hbs",
244+
"toc.html.hbs",
240245
"favicon.png",
241246
"favicon.svg",
242247
"css/chrome.css",
@@ -268,7 +273,8 @@ mod tests {
268273
head: Vec::new(),
269274
redirect: Vec::new(),
270275
header: Vec::new(),
271-
toc: Vec::new(),
276+
toc_js: Vec::new(),
277+
toc_html: Vec::new(),
272278
chrome_css: Vec::new(),
273279
general_css: Vec::new(),
274280
print_css: Vec::new(),

src/theme/toc.html.hbs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<!DOCTYPE HTML>
2+
<html lang="{{ language }}" class="{{ default_theme }}" dir="{{ text_direction }}">
3+
<head>
4+
<!-- sidebar iframe generated using mdBook
5+
6+
This is a frame, and not included directly in the page, to control the total size of the
7+
book. The TOC contains an entry for each page, so if each page includes a copy of the TOC,
8+
the total size of the page becomes O(n**2).
9+
10+
The frame is only used as a fallback when JS is turned off. When it's on, the sidebar is
11+
instead added to the main page by `toc.js` instead. The JavaScript mode is better
12+
because, when running in a `file:///` URL, the iframed page would not be Same-Origin as
13+
the rest of the page, so the sidebar and the main page theme would fall out of sync.
14+
-->
15+
<meta charset="UTF-8">
16+
<meta name="robots" content="noindex">
17+
{{#if base_url}}
18+
<base href="{{ base_url }}">
19+
{{/if}}
20+
<!-- Custom HTML head -->
21+
{{> head}}
22+
<meta name="viewport" content="width=device-width, initial-scale=1">
23+
<meta name="theme-color" content="#ffffff">
24+
<link rel="stylesheet" href="{{ path_to_root }}css/variables.css">
25+
<link rel="stylesheet" href="{{ path_to_root }}css/general.css">
26+
<link rel="stylesheet" href="{{ path_to_root }}css/chrome.css">
27+
{{#if print_enable}}
28+
<link rel="stylesheet" href="{{ path_to_root }}css/print.css" media="print">
29+
{{/if}}
30+
<!-- Fonts -->
31+
<link rel="stylesheet" href="{{ path_to_root }}FontAwesome/css/font-awesome.css">
32+
{{#if copy_fonts}}
33+
<link rel="stylesheet" href="{{ path_to_root }}fonts/fonts.css">
34+
{{/if}}
35+
<!-- Custom theme stylesheets -->
36+
{{#each additional_css}}
37+
<link rel="stylesheet" href="{{ ../path_to_root }}{{ this }}">
38+
{{/each}}
39+
</head>
40+
<body class="sidebar-iframe-inner">
41+
{{#toc}}{{/toc}}
42+
</body>
43+
</html>

tests/rendered_output.rs

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use mdbook::utils::fs::write_file;
99
use mdbook::MDBook;
1010
use pretty_assertions::assert_eq;
1111
use select::document::Document;
12-
use select::predicate::{Class, Name, Predicate};
12+
use select::predicate::{Attr, Class, Name, Predicate};
1313
use std::collections::HashMap;
1414
use std::ffi::OsStr;
1515
use std::fs;
@@ -232,7 +232,7 @@ fn entry_ends_with(entry: &DirEntry, ending: &str) -> bool {
232232

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

255+
/// Read the TOC fallback (`book/toc.html`) HTML and expose it as a DOM which we
256+
/// can search with the `select` crate
257+
fn toc_fallback_html() -> Result<Document> {
258+
let temp = DummyBook::new()
259+
.build()
260+
.with_context(|| "Couldn't create the dummy book")?;
261+
MDBook::load(temp.path())?
262+
.build()
263+
.with_context(|| "Book building failed")?;
264+
265+
let toc_path = temp.path().join("book").join("toc.html");
266+
let html = fs::read_to_string(toc_path).with_context(|| "Unable to read index.html")?;
267+
Ok(Document::from(html.as_str()))
268+
}
269+
255270
#[test]
256271
fn check_second_toc_level() {
257-
let doc = toc_html().unwrap();
272+
let doc = toc_js_html().unwrap();
258273
let mut should_be = Vec::from(TOC_SECOND_LEVEL);
259274
should_be.sort_unstable();
260275

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

277292
#[test]
278293
fn check_first_toc_level() {
279-
let doc = toc_html().unwrap();
294+
let doc = toc_js_html().unwrap();
280295
let mut should_be = Vec::from(TOC_TOP_LEVEL);
281296

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

300315
#[test]
301316
fn check_spacers() {
302-
let doc = toc_html().unwrap();
317+
let doc = toc_js_html().unwrap();
303318
let should_be = 2;
304319

305320
let num_spacers = doc
@@ -308,6 +323,39 @@ fn check_spacers() {
308323
assert_eq!(num_spacers, should_be);
309324
}
310325

326+
// don't use target="_parent" in JS
327+
#[test]
328+
fn check_link_target_js() {
329+
let doc = toc_js_html().unwrap();
330+
331+
let num_parent_links = doc
332+
.find(
333+
Class("chapter")
334+
.descendant(Name("li"))
335+
.descendant(Name("a").and(Attr("target", "_parent"))),
336+
)
337+
.count();
338+
assert_eq!(num_parent_links, 0);
339+
}
340+
341+
// don't use target="_parent" in IFRAME
342+
#[test]
343+
fn check_link_target_fallback() {
344+
let doc = toc_fallback_html().unwrap();
345+
346+
let num_parent_links = doc
347+
.find(
348+
Class("chapter")
349+
.descendant(Name("li"))
350+
.descendant(Name("a").and(Attr("target", "_parent"))),
351+
)
352+
.count();
353+
assert_eq!(
354+
num_parent_links,
355+
TOC_TOP_LEVEL.len() + TOC_SECOND_LEVEL.len()
356+
);
357+
}
358+
311359
/// Ensure building fails if `create-missing` is false and one of the files does
312360
/// not exist.
313361
#[test]

0 commit comments

Comments
 (0)