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.
This commit is contained in:
parent
2cb5b85ab2
commit
203685e91c
7 changed files with 148 additions and 14 deletions
|
|
@ -529,7 +529,9 @@ impl Renderer for HtmlHandlebars {
|
||||||
handlebars.register_partial("header", String::from_utf8(theme.header.clone())?)?;
|
handlebars.register_partial("header", String::from_utf8(theme.header.clone())?)?;
|
||||||
|
|
||||||
debug!("Register the toc handlebars template");
|
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");
|
debug!("Register handlebars helpers");
|
||||||
self.register_hbs_helpers(&mut handlebars, &html_config);
|
self.register_hbs_helpers(&mut handlebars, &html_config);
|
||||||
|
|
@ -586,11 +588,16 @@ impl Renderer for HtmlHandlebars {
|
||||||
debug!("Creating print.html ✓");
|
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())?;
|
utils::fs::write_file(destination, "toc.js", rendered_toc.as_bytes())?;
|
||||||
debug!("Creating toc.js ✓");
|
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");
|
debug!("Copy static files");
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,13 @@ impl HelperDef for RenderToc {
|
||||||
RenderErrorReason::Other("Type error for `fold_level`, u64 expected".to_owned())
|
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\">")?;
|
out.write("<ol class=\"chapter\">")?;
|
||||||
|
|
||||||
let mut current_level = 1;
|
let mut current_level = 1;
|
||||||
|
|
@ -113,7 +120,11 @@ impl HelperDef for RenderToc {
|
||||||
|
|
||||||
// Add link
|
// Add link
|
||||||
out.write(&tmp)?;
|
out.write(&tmp)?;
|
||||||
out.write("\">")?;
|
out.write(if is_toc_html {
|
||||||
|
"\" target=\"_parent\">"
|
||||||
|
} else {
|
||||||
|
"\">"
|
||||||
|
})?;
|
||||||
path_exists = true;
|
path_exists = true;
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
|
|
||||||
|
|
@ -399,6 +399,22 @@ ul#searchresults span.teaser em {
|
||||||
background-color: var(--sidebar-bg);
|
background-color: var(--sidebar-bg);
|
||||||
color: var(--sidebar-fg);
|
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; }
|
[dir=rtl] .sidebar { left: unset; right: 0; }
|
||||||
.sidebar-resizing {
|
.sidebar-resizing {
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,9 @@
|
||||||
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||||
<!-- populated by js -->
|
<!-- populated by js -->
|
||||||
<div class="sidebar-scrollbox"></div>
|
<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 id="sidebar-resize-handle" class="sidebar-resize-handle">
|
||||||
<div class="sidebar-resize-indicator"></div>
|
<div class="sidebar-resize-indicator"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@ pub static INDEX: &[u8] = include_bytes!("index.hbs");
|
||||||
pub static HEAD: &[u8] = include_bytes!("head.hbs");
|
pub static HEAD: &[u8] = include_bytes!("head.hbs");
|
||||||
pub static REDIRECT: &[u8] = include_bytes!("redirect.hbs");
|
pub static REDIRECT: &[u8] = include_bytes!("redirect.hbs");
|
||||||
pub static HEADER: &[u8] = include_bytes!("header.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 CHROME_CSS: &[u8] = include_bytes!("css/chrome.css");
|
||||||
pub static GENERAL_CSS: &[u8] = include_bytes!("css/general.css");
|
pub static GENERAL_CSS: &[u8] = include_bytes!("css/general.css");
|
||||||
pub static PRINT_CSS: &[u8] = include_bytes!("css/print.css");
|
pub static PRINT_CSS: &[u8] = include_bytes!("css/print.css");
|
||||||
|
|
@ -51,7 +52,8 @@ pub struct Theme {
|
||||||
pub head: Vec<u8>,
|
pub head: Vec<u8>,
|
||||||
pub redirect: Vec<u8>,
|
pub redirect: Vec<u8>,
|
||||||
pub header: 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 chrome_css: Vec<u8>,
|
||||||
pub general_css: Vec<u8>,
|
pub general_css: Vec<u8>,
|
||||||
pub print_css: Vec<u8>,
|
pub print_css: Vec<u8>,
|
||||||
|
|
@ -87,7 +89,8 @@ impl Theme {
|
||||||
(theme_dir.join("head.hbs"), &mut theme.head),
|
(theme_dir.join("head.hbs"), &mut theme.head),
|
||||||
(theme_dir.join("redirect.hbs"), &mut theme.redirect),
|
(theme_dir.join("redirect.hbs"), &mut theme.redirect),
|
||||||
(theme_dir.join("header.hbs"), &mut theme.header),
|
(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("book.js"), &mut theme.js),
|
||||||
(theme_dir.join("css/chrome.css"), &mut theme.chrome_css),
|
(theme_dir.join("css/chrome.css"), &mut theme.chrome_css),
|
||||||
(theme_dir.join("css/general.css"), &mut theme.general_css),
|
(theme_dir.join("css/general.css"), &mut theme.general_css),
|
||||||
|
|
@ -177,7 +180,8 @@ impl Default for Theme {
|
||||||
head: HEAD.to_owned(),
|
head: HEAD.to_owned(),
|
||||||
redirect: REDIRECT.to_owned(),
|
redirect: REDIRECT.to_owned(),
|
||||||
header: HEADER.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(),
|
chrome_css: CHROME_CSS.to_owned(),
|
||||||
general_css: GENERAL_CSS.to_owned(),
|
general_css: GENERAL_CSS.to_owned(),
|
||||||
print_css: PRINT_CSS.to_owned(),
|
print_css: PRINT_CSS.to_owned(),
|
||||||
|
|
@ -237,6 +241,7 @@ mod tests {
|
||||||
"redirect.hbs",
|
"redirect.hbs",
|
||||||
"header.hbs",
|
"header.hbs",
|
||||||
"toc.js.hbs",
|
"toc.js.hbs",
|
||||||
|
"toc.html.hbs",
|
||||||
"favicon.png",
|
"favicon.png",
|
||||||
"favicon.svg",
|
"favicon.svg",
|
||||||
"css/chrome.css",
|
"css/chrome.css",
|
||||||
|
|
@ -268,7 +273,8 @@ mod tests {
|
||||||
head: Vec::new(),
|
head: Vec::new(),
|
||||||
redirect: Vec::new(),
|
redirect: Vec::new(),
|
||||||
header: Vec::new(),
|
header: Vec::new(),
|
||||||
toc: Vec::new(),
|
toc_js: Vec::new(),
|
||||||
|
toc_html: Vec::new(),
|
||||||
chrome_css: Vec::new(),
|
chrome_css: Vec::new(),
|
||||||
general_css: Vec::new(),
|
general_css: Vec::new(),
|
||||||
print_css: Vec::new(),
|
print_css: Vec::new(),
|
||||||
|
|
|
||||||
43
src/theme/toc.html.hbs
Normal file
43
src/theme/toc.html.hbs
Normal file
|
|
@ -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>
|
||||||
|
|
@ -9,7 +9,7 @@ use mdbook::utils::fs::write_file;
|
||||||
use mdbook::MDBook;
|
use mdbook::MDBook;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use select::document::Document;
|
use select::document::Document;
|
||||||
use select::predicate::{Class, Name, Predicate};
|
use select::predicate::{Attr, Class, Name, Predicate};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
@ -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
|
/// Read the TOC (`book/toc.js`) nested HTML and expose it as a DOM which we
|
||||||
/// can search with the `select` crate
|
/// can search with the `select` crate
|
||||||
fn toc_html() -> Result<Document> {
|
fn toc_js_html() -> Result<Document> {
|
||||||
let temp = DummyBook::new()
|
let temp = DummyBook::new()
|
||||||
.build()
|
.build()
|
||||||
.with_context(|| "Couldn't create the dummy book")?;
|
.with_context(|| "Couldn't create the dummy book")?;
|
||||||
|
|
@ -252,9 +252,24 @@ fn toc_html() -> Result<Document> {
|
||||||
panic!("cannot find toc in file")
|
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]
|
#[test]
|
||||||
fn check_second_toc_level() {
|
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);
|
let mut should_be = Vec::from(TOC_SECOND_LEVEL);
|
||||||
should_be.sort_unstable();
|
should_be.sort_unstable();
|
||||||
|
|
||||||
|
|
@ -276,7 +291,7 @@ fn check_second_toc_level() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_first_toc_level() {
|
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);
|
let mut should_be = Vec::from(TOC_TOP_LEVEL);
|
||||||
|
|
||||||
should_be.extend(TOC_SECOND_LEVEL);
|
should_be.extend(TOC_SECOND_LEVEL);
|
||||||
|
|
@ -299,7 +314,7 @@ fn check_first_toc_level() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_spacers() {
|
fn check_spacers() {
|
||||||
let doc = toc_html().unwrap();
|
let doc = toc_js_html().unwrap();
|
||||||
let should_be = 2;
|
let should_be = 2;
|
||||||
|
|
||||||
let num_spacers = doc
|
let num_spacers = doc
|
||||||
|
|
@ -308,6 +323,39 @@ fn check_spacers() {
|
||||||
assert_eq!(num_spacers, should_be);
|
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
|
/// Ensure building fails if `create-missing` is false and one of the files does
|
||||||
/// not exist.
|
/// not exist.
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue