1 use crate::book
::{Book, BookItem}
;
2 use crate::config
::{Config, HtmlConfig, Playpen}
;
4 use crate::renderer
::html_handlebars
::helpers
;
5 use crate::renderer
::{RenderContext, Renderer}
;
6 use crate::theme
::{self, playpen_editor, Theme}
;
10 use std
::collections
::BTreeMap
;
11 use std
::collections
::HashMap
;
13 use std
::path
::{Path, PathBuf}
;
15 use handlebars
::Handlebars
;
16 use regex
::{Captures, Regex}
;
19 pub struct HtmlHandlebars
;
22 pub fn new() -> Self {
29 mut ctx
: RenderItemContext
<'_
>,
30 print_content
: &mut String
,
32 // FIXME: This should be made DRY-er and rely less on mutable state
33 if let BookItem
::Chapter(ref ch
) = *item
{
34 let content
= ch
.content
.clone();
35 let content
= utils
::render_markdown(&content
, ctx
.html_config
.curly_quotes
);
37 let fixed_content
= utils
::render_markdown_with_path(
39 ctx
.html_config
.curly_quotes
,
42 print_content
.push_str(&fixed_content
);
44 // Update the context with data for this file
48 .chain_err(|| "Could not convert path to str")?
;
49 let filepath
= Path
::new(&ch
.path
).with_extension("html");
51 // "print.html" is used for the print page.
52 if ch
.path
== Path
::new("print.md") {
53 bail
!(ErrorKind
::ReservedFilenameError(ch
.path
.clone()));
56 // Non-lexical lifetimes needed :'(
62 .and_then(serde_json
::Value
::as_str
)
65 title
= match book_title
{
66 "" => ch
.name
.clone(),
67 _
=> ch
.name
.clone() + " - " + book_title
,
71 ctx
.data
.insert("path".to_owned(), json
!(path
));
72 ctx
.data
.insert("content".to_owned(), json
!(content
));
73 ctx
.data
.insert("chapter_title".to_owned(), json
!(ch
.name
));
74 ctx
.data
.insert("title".to_owned(), json
!(title
));
76 "path_to_root".to_owned(),
77 json
!(utils
::fs
::path_to_root(&ch
.path
)),
79 if let Some(ref section
) = ch
.number
{
81 .insert("section".to_owned(), json
!(section
.to_string()));
84 // Render the handlebars template with the data
85 debug
!("Render template");
86 let rendered
= ctx
.handlebars
.render("index", &ctx
.data
)?
;
88 let rendered
= self.post_process(rendered
, &ctx
.html_config
.playpen
);
91 debug
!("Creating {}", filepath
.display());
92 utils
::fs
::write_file(&ctx
.destination
, &filepath
, rendered
.as_bytes())?
;
95 ctx
.data
.insert("path".to_owned(), json
!("index.md"));
96 ctx
.data
.insert("path_to_root".to_owned(), json
!(""));
97 ctx
.data
.insert("is_index".to_owned(), json
!("true"));
98 let rendered_index
= ctx
.handlebars
.render("index", &ctx
.data
)?
;
99 let rendered_index
= self.post_process(rendered_index
, &ctx
.html_config
.playpen
);
100 debug
!("Creating index.html from {}", path
);
101 utils
::fs
::write_file(&ctx
.destination
, "index.html", rendered_index
.as_bytes())?
;
108 #[cfg_attr(feature = "cargo-clippy", allow(clippy::let_and_return))]
109 fn post_process(&self, rendered
: String
, playpen_config
: &Playpen
) -> String
{
110 let rendered
= build_header_links(&rendered
);
111 let rendered
= fix_code_blocks(&rendered
);
112 let rendered
= add_playpen_pre(&rendered
, playpen_config
);
117 fn copy_static_files(
121 html_config
: &HtmlConfig
,
123 use crate::utils
::fs
::write_file
;
128 b
"This file makes sure that Github Pages doesn't process mdBook's output.",
131 write_file(destination
, "book.js", &theme
.js
)?
;
132 write_file(destination
, "css/general.css", &theme
.general_css
)?
;
133 write_file(destination
, "css/chrome.css", &theme
.chrome_css
)?
;
134 write_file(destination
, "css/print.css", &theme
.print_css
)?
;
135 write_file(destination
, "css/variables.css", &theme
.variables_css
)?
;
136 write_file(destination
, "favicon.png", &theme
.favicon
)?
;
137 write_file(destination
, "highlight.css", &theme
.highlight_css
)?
;
138 write_file(destination
, "tomorrow-night.css", &theme
.tomorrow_night_css
)?
;
139 write_file(destination
, "ayu-highlight.css", &theme
.ayu_highlight_css
)?
;
140 write_file(destination
, "highlight.js", &theme
.highlight_js
)?
;
141 write_file(destination
, "clipboard.min.js", &theme
.clipboard_js
)?
;
144 "FontAwesome/css/font-awesome.css",
149 "FontAwesome/fonts/fontawesome-webfont.eot",
150 theme
::FONT_AWESOME_EOT
,
154 "FontAwesome/fonts/fontawesome-webfont.svg",
155 theme
::FONT_AWESOME_SVG
,
159 "FontAwesome/fonts/fontawesome-webfont.ttf",
160 theme
::FONT_AWESOME_TTF
,
164 "FontAwesome/fonts/fontawesome-webfont.woff",
165 theme
::FONT_AWESOME_WOFF
,
169 "FontAwesome/fonts/fontawesome-webfont.woff2",
170 theme
::FONT_AWESOME_WOFF2
,
174 "FontAwesome/fonts/FontAwesome.ttf",
175 theme
::FONT_AWESOME_TTF
,
178 let playpen_config
= &html_config
.playpen
;
180 // Ace is a very large dependency, so only load it when requested
181 if playpen_config
.editable
&& playpen_config
.copy_js
{
183 write_file(destination
, "editor.js", playpen_editor
::JS
)?
;
184 write_file(destination
, "ace.js", playpen_editor
::ACE_JS
)?
;
185 write_file(destination
, "mode-rust.js", playpen_editor
::MODE_RUST_JS
)?
;
186 write_file(destination
, "theme-dawn.js", playpen_editor
::THEME_DAWN_JS
)?
;
189 "theme-tomorrow_night.js",
190 playpen_editor
::THEME_TOMORROW_NIGHT_JS
,
197 /// Update the context with data for this file
198 fn configure_print_version(
200 data
: &mut serde_json
::Map
<String
, serde_json
::Value
>,
203 // Make sure that the Print chapter does not display the title from
204 // the last rendered chapter by removing it from its context
205 data
.remove("title");
206 data
.insert("is_print".to_owned(), json
!(true));
207 data
.insert("path".to_owned(), json
!("print.md"));
208 data
.insert("content".to_owned(), json
!(print_content
));
210 "path_to_root".to_owned(),
211 json
!(utils
::fs
::path_to_root(Path
::new("print.md"))),
215 fn register_hbs_helpers(&self, handlebars
: &mut Handlebars
<'_
>, html_config
: &HtmlConfig
) {
216 handlebars
.register_helper(
218 Box
::new(helpers
::toc
::RenderToc
{
219 no_section_label
: html_config
.no_section_label
,
222 handlebars
.register_helper("previous", Box
::new(helpers
::navigation
::previous
));
223 handlebars
.register_helper("next", Box
::new(helpers
::navigation
::next
));
224 handlebars
.register_helper("theme_option", Box
::new(helpers
::theme
::theme_option
));
227 /// Copy across any additional CSS and JavaScript files which the book
228 /// has been configured to use.
229 fn copy_additional_css_and_js(
235 let custom_files
= html
.additional_css
.iter().chain(html
.additional_js
.iter());
237 debug
!("Copying additional CSS and JS");
239 for custom_file
in custom_files
{
240 let input_location
= root
.join(custom_file
);
241 let output_location
= destination
.join(custom_file
);
242 if let Some(parent
) = output_location
.parent() {
243 fs
::create_dir_all(parent
)
244 .chain_err(|| format
!("Unable to create {}", parent
.display()))?
;
248 input_location
.display(),
249 output_location
.display()
252 fs
::copy(&input_location
, &output_location
).chain_err(|| {
254 "Unable to copy {} to {}",
255 input_location
.display(),
256 output_location
.display()
265 // TODO(mattico): Remove some time after the 0.1.8 release
266 fn maybe_wrong_theme_dir(dir
: &Path
) -> Result
<bool
> {
267 fn entry_is_maybe_book_file(entry
: fs
::DirEntry
) -> Result
<bool
> {
268 Ok(entry
.file_type()?
.is_file()
269 && entry
.path().extension().map_or(false, |ext
| ext
== "md"))
273 for entry
in fs
::read_dir(dir
)?
{
274 if entry_is_maybe_book_file(entry?
).unwrap_or(false) {
284 impl Renderer
for HtmlHandlebars
{
285 fn name(&self) -> &str {
289 fn render(&self, ctx
: &RenderContext
) -> Result
<()> {
290 let html_config
= ctx
.config
.html_config().unwrap_or_default();
291 let src_dir
= ctx
.root
.join(&ctx
.config
.book
.src
);
292 let destination
= &ctx
.destination
;
293 let book
= &ctx
.book
;
294 let build_dir
= ctx
.root
.join(&ctx
.config
.build
.build_dir
);
296 if destination
.exists() {
297 utils
::fs
::remove_dir_content(destination
)
298 .chain_err(|| "Unable to remove stale HTML output")?
;
302 let mut handlebars
= Handlebars
::new();
304 let theme_dir
= match html_config
.theme
{
305 Some(ref theme
) => theme
.to_path_buf(),
306 None
=> ctx
.root
.join("theme"),
309 if html_config
.theme
.is_none()
310 && maybe_wrong_theme_dir(&src_dir
.join("theme")).unwrap_or(false)
313 "Previous versions of mdBook erroneously accepted `./src/theme` as an automatic \
316 warn
!("Please move your theme files to `./theme` for them to continue being used");
319 let theme
= theme
::Theme
::new(theme_dir
);
321 debug
!("Register the index handlebars template");
322 handlebars
.register_template_string("index", String
::from_utf8(theme
.index
.clone())?
)?
;
324 debug
!("Register the header handlebars template");
325 handlebars
.register_partial("header", String
::from_utf8(theme
.header
.clone())?
)?
;
327 debug
!("Register handlebars helpers");
328 self.register_hbs_helpers(&mut handlebars
, &html_config
);
330 let mut data
= make_data(&ctx
.root
, &book
, &ctx
.config
, &html_config
)?
;
333 let mut print_content
= String
::new();
335 fs
::create_dir_all(&destination
)
336 .chain_err(|| "Unexpected error when constructing destination path")?
;
338 let mut is_index
= true;
339 for item
in book
.iter() {
340 let ctx
= RenderItemContext
{
341 handlebars
: &handlebars
,
342 destination
: destination
.to_path_buf(),
345 html_config
: html_config
.clone(),
347 self.render_item(item
, ctx
, &mut print_content
)?
;
352 self.configure_print_version(&mut data
, &print_content
);
353 if let Some(ref title
) = ctx
.config
.book
.title
{
354 data
.insert("title".to_owned(), json
!(title
));
357 // Render the handlebars template with the data
358 debug
!("Render template");
359 let rendered
= handlebars
.render("index", &data
)?
;
361 let rendered
= self.post_process(rendered
, &html_config
.playpen
);
363 utils
::fs
::write_file(&destination
, "print.html", rendered
.as_bytes())?
;
364 debug
!("Creating print.html ✓");
366 debug
!("Copy static files");
367 self.copy_static_files(&destination
, &theme
, &html_config
)
368 .chain_err(|| "Unable to copy across static files")?
;
369 self.copy_additional_css_and_js(&html_config
, &ctx
.root
, &destination
)
370 .chain_err(|| "Unable to copy across additional CSS and JS")?
;
372 // Render search index
373 #[cfg(feature = "search")]
375 let search
= html_config
.search
.unwrap_or_default();
377 super::search
::create_files(&search
, &destination
, &book
)?
;
381 // Copy all remaining files, avoid a recursive copy from/to the book build dir
382 utils
::fs
::copy_files_except_ext(&src_dir
, &destination
, true, Some(&build_dir
), &["md"])?
;
392 html_config
: &HtmlConfig
,
393 ) -> Result
<serde_json
::Map
<String
, serde_json
::Value
>> {
396 let mut data
= serde_json
::Map
::new();
398 "language".to_owned(),
399 json
!(config
.book
.language
.clone().unwrap_or_default()),
402 "book_title".to_owned(),
403 json
!(config
.book
.title
.clone().unwrap_or_default()),
406 "description".to_owned(),
407 json
!(config
.book
.description
.clone().unwrap_or_default()),
409 data
.insert("favicon".to_owned(), json
!("favicon.png"));
410 if let Some(ref livereload
) = html_config
.livereload_url
{
411 data
.insert("livereload".to_owned(), json
!(livereload
));
414 let default_theme
= match html_config
.default_theme
{
415 Some(ref theme
) => theme
.to_lowercase(),
416 None
=> "light".to_string(),
418 data
.insert("default_theme".to_owned(), json
!(default_theme
));
420 let preferred_dark_theme
= match html_config
.preferred_dark_theme
{
421 Some(ref theme
) => theme
.to_lowercase(),
422 None
=> default_theme
,
425 "preferred_dark_theme".to_owned(),
426 json
!(preferred_dark_theme
),
429 // Add google analytics tag
430 if let Some(ref ga
) = html_config
.google_analytics
{
431 data
.insert("google_analytics".to_owned(), json
!(ga
));
434 if html_config
.mathjax_support
{
435 data
.insert("mathjax_support".to_owned(), json
!(true));
438 // Add check to see if there is an additional style
439 if !html_config
.additional_css
.is_empty() {
440 let mut css
= Vec
::new();
441 for style
in &html_config
.additional_css
{
442 match style
.strip_prefix(root
) {
443 Ok(p
) => css
.push(p
.to_str().expect("Could not convert to str")),
444 Err(_
) => css
.push(style
.to_str().expect("Could not convert to str")),
447 data
.insert("additional_css".to_owned(), json
!(css
));
450 // Add check to see if there is an additional script
451 if !html_config
.additional_js
.is_empty() {
452 let mut js
= Vec
::new();
453 for script
in &html_config
.additional_js
{
454 match script
.strip_prefix(root
) {
455 Ok(p
) => js
.push(p
.to_str().expect("Could not convert to str")),
456 Err(_
) => js
.push(script
.to_str().expect("Could not convert to str")),
459 data
.insert("additional_js".to_owned(), json
!(js
));
462 if html_config
.playpen
.editable
&& html_config
.playpen
.copy_js
{
463 data
.insert("playpen_js".to_owned(), json
!(true));
464 if html_config
.playpen
.line_numbers
{
465 data
.insert("playpen_line_numbers".to_owned(), json
!(true));
468 if html_config
.playpen
.copyable
{
469 data
.insert("playpen_copyable".to_owned(), json
!(true));
472 data
.insert("fold_enable".to_owned(), json
!((html_config
.fold
.enable
)));
473 data
.insert("fold_level".to_owned(), json
!((html_config
.fold
.level
)));
475 let search
= html_config
.search
.clone();
476 if cfg
!(feature
= "search") {
477 let search
= search
.unwrap_or_default();
478 data
.insert("search_enabled".to_owned(), json
!(search
.enable
));
480 "search_js".to_owned(),
481 json
!(search
.enable
&& search
.copy_js
),
483 } else if search
.is_some() {
484 warn
!("mdBook compiled without search support, ignoring `output.html.search` table");
486 "please reinstall with `cargo install mdbook --force --features search`to use the \
491 if let Some(ref git_repository_url
) = html_config
.git_repository_url
{
492 data
.insert("git_repository_url".to_owned(), json
!(git_repository_url
));
495 let git_repository_icon
= match html_config
.git_repository_icon
{
496 Some(ref git_repository_icon
) => git_repository_icon
,
499 data
.insert("git_repository_icon".to_owned(), json
!(git_repository_icon
));
501 let mut chapters
= vec
![];
503 for item
in book
.iter() {
504 // Create the data to inject in the template
505 let mut chapter
= BTreeMap
::new();
508 BookItem
::Chapter(ref ch
) => {
509 if let Some(ref section
) = ch
.number
{
510 chapter
.insert("section".to_owned(), json
!(section
.to_string()));
514 "has_sub_items".to_owned(),
515 json
!((!ch
.sub_items
.is_empty()).to_string()),
518 chapter
.insert("name".to_owned(), json
!(ch
.name
));
522 .chain_err(|| "Could not convert path to str")?
;
523 chapter
.insert("path".to_owned(), json
!(path
));
525 BookItem
::Separator
=> {
526 chapter
.insert("spacer".to_owned(), json
!("_spacer_"));
530 chapters
.push(chapter
);
533 data
.insert("chapters".to_owned(), json
!(chapters
));
535 debug
!("[*]: JSON constructed");
539 /// Goes through the rendered HTML, making sure all header tags have
540 /// an anchor respectively so people can link to sections directly.
541 fn build_header_links(html
: &str) -> String
{
542 let regex
= Regex
::new(r
"<h(\d)>(.*?)</h\d>").unwrap();
543 let mut id_counter
= HashMap
::new();
546 .replace_all(html
, |caps
: &Captures
<'_
>| {
549 .expect("Regex should ensure we only ever get numbers here");
551 insert_link_into_header(level
, &caps
[2], &mut id_counter
)
556 /// Insert a sinle link into a header, making sure each link gets its own
557 /// unique ID by appending an auto-incremented number (if necessary).
558 fn insert_link_into_header(
561 id_counter
: &mut HashMap
<String
, usize>,
563 let raw_id
= utils
::id_from_content(content
);
565 let id_count
= id_counter
.entry(raw_id
.clone()).or_insert(0);
567 let id
= match *id_count
{
569 other
=> format
!("{}-{}", raw_id
, other
),
575 r
##"<h{level}><a class="header" href="#{id}" id="{id}">{text}</a></h{level}>"##,
582 // The rust book uses annotations for rustdoc to test code snippets,
583 // like the following:
584 // ```rust,should_panic
589 // This function replaces all commas by spaces in the code block classes
590 fn fix_code_blocks(html
: &str) -> String
{
591 let regex
= Regex
::new(r
##"<code([^>]+)class="([^"]+)"([^>]*)>"##).unwrap();
593 .replace_all(html, |caps: &Captures<'_>| {
594 let before = &caps[1];
595 let classes = &caps[2].replace(",", " ");
596 let after = &caps[3];
599 r#"<code{before}class="{classes}"{after}>"#,
608 fn add_playpen_pre(html: &str, playpen_config: &Playpen) -> String {
609 let regex = Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap();
611 .replace_all(html
, |caps
: &Captures
<'_
>| {
613 let classes
= &caps
[2];
616 if classes
.contains("language-rust") {
617 if (!classes
.contains("ignore") && !classes
.contains("noplaypen"))
618 || classes
.contains("mdbook-runnable")
620 // wrap the contents in an external pre block
622 "<pre class=\"playpen\"><code class=\"{}\">{}</code></pre>",
625 let content
: Cow
<'_
, str> = if playpen_config
.editable
626 && classes
.contains("editable")
627 || text
.contains("fn main")
628 || text
.contains("quick_main!")
632 // we need to inject our own main
633 let (attrs
, code
) = partition_source(code
);
636 "\n# #![allow(unused_variables)]\n{}#fn main() {{\n{}#}}",
645 format
!("<code class=\"{}\">{}</code>", classes
, hide_lines(code
))
648 // not language-rust, so no-op
656 static ref BORING_LINES_REGEX
: Regex
= Regex
::new(r
"^(\s*)#(.?)(.*)$").unwrap();
659 fn hide_lines(content
: &str) -> String
{
660 let mut result
= String
::with_capacity(content
.len());
661 for line
in content
.lines() {
662 if let Some(caps
) = BORING_LINES_REGEX
.captures(line
) {
669 } else if &caps
[2] != "!" && &caps
[2] != "[" {
670 result
+= "<span class=\"boring\">";
687 fn partition_source(s
: &str) -> (String
, String
) {
688 let mut after_header
= false;
689 let mut before
= String
::new();
690 let mut after
= String
::new();
692 for line
in s
.lines() {
693 let trimline
= line
.trim();
694 let header
= trimline
.chars().all(char::is_whitespace
) || trimline
.starts_with("#![");
695 if !header
|| after_header
{
697 after
.push_str(line
);
698 after
.push_str("\n");
700 before
.push_str(line
);
701 before
.push_str("\n");
708 struct RenderItemContext
<'a
> {
709 handlebars
: &'a Handlebars
<'a
>,
710 destination
: PathBuf
,
711 data
: serde_json
::Map
<String
, serde_json
::Value
>,
713 html_config
: HtmlConfig
,
721 fn original_build_header_links() {
724 "blah blah <h1>Foo</h1>",
725 r
##"blah blah <h1><a class="header" href="#foo" id="foo">Foo</a></h1>"##,
729 r
##"<h1><a class="header" href="#foo" id="foo">Foo</a></h1>"##,
733 r
##"<h3><a class="header" href="#foobar" id="foobar">Foo^bar</a></h3>"##,
737 r
##"<h4><a class="header" href="#" id=""></a></h4>"##,
740 "<h4><em>Hï</em></h4>",
741 r
##"<h4><a class="header" href="#hï" id="hï"><em>Hï</em></a></h4>"##,
744 "<h1>Foo</h1><h3>Foo</h3>",
745 r
##"<h1><a class="header" href="#foo" id="foo">Foo</a></h1><h3><a class="header" href="#foo-1" id="foo-1">Foo</a></h3>"##,
749 for (src
, should_be
) in inputs
{
750 let got
= build_header_links(&src
);
751 assert_eq
!(got
, should_be
);
758 ("<code class=\"language-rust\">x()</code>",
759 "<pre class=\"playpen\"><code class=\"language-rust\">\n<span class=\"boring\">#![allow(unused_variables)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
760 ("<code class=\"language-rust\">fn main() {}</code>",
761 "<pre class=\"playpen\"><code class=\"language-rust\">fn main() {}\n</code></pre>"),
762 ("<code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code>",
763 "<pre class=\"playpen\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";\n</code></pre>"),
764 ("<code class=\"language-rust editable\">let s = \"foo\n ## bar\n\";</code>",
765 "<pre class=\"playpen\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n\";\n</code></pre>"),
766 ("<code class=\"language-rust editable\">let s = \"foo\n # bar\n#\n\";</code>",
767 "<pre class=\"playpen\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span><span class=\"boring\">\n</span>\";\n</code></pre>"),
768 ("<code class=\"language-rust ignore\">let s = \"foo\n # bar\n\";</code>",
769 "<code class=\"language-rust ignore\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";\n</code>"),
770 ("<code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]</code>",
771 "<pre class=\"playpen\"><code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]\n</code></pre>"),
773 for (src
, should_be
) in &inputs
{
774 let got
= add_playpen_pre(
781 assert_eq
!(&*got
, *should_be
);