1 use crate::book::{Book, BookItem};
2 use crate::config::{BookConfig, Config, HtmlConfig, Playground, RustEdition};
3 use crate::errors::*;
4 use crate::renderer::html_handlebars::helpers;
5 use crate::renderer::{RenderContext, Renderer};
6 use crate::theme::{self, playground_editor, Theme};
7 use crate::utils;
8 
9 use std::borrow::Cow;
10 use std::collections::BTreeMap;
11 use std::collections::HashMap;
12 use std::fs::{self, File};
13 use std::path::{Path, PathBuf};
14 
15 use crate::utils::fs::get_404_output_file;
16 use handlebars::Handlebars;
17 use regex::{Captures, Regex};
18 
19 #[derive(Default)]
20 pub struct HtmlHandlebars;
21 
22 impl HtmlHandlebars {
new() -> Self23     pub fn new() -> Self {
24         HtmlHandlebars
25     }
26 
render_item( &self, item: &BookItem, mut ctx: RenderItemContext<'_>, print_content: &mut String, ) -> Result<()>27     fn render_item(
28         &self,
29         item: &BookItem,
30         mut ctx: RenderItemContext<'_>,
31         print_content: &mut String,
32     ) -> Result<()> {
33         // FIXME: This should be made DRY-er and rely less on mutable state
34 
35         let (ch, path) = match item {
36             BookItem::Chapter(ch) if !ch.is_draft_chapter() => (ch, ch.path.as_ref().unwrap()),
37             _ => return Ok(()),
38         };
39 
40         if let Some(ref edit_url_template) = ctx.html_config.edit_url_template {
41             let full_path = ctx.book_config.src.to_str().unwrap_or_default().to_owned()
42                 + "/"
43                 + ch.source_path
44                     .clone()
45                     .unwrap_or_default()
46                     .to_str()
47                     .unwrap_or_default();
48 
49             let edit_url = edit_url_template.replace("{path}", &full_path);
50             ctx.data
51                 .insert("git_repository_edit_url".to_owned(), json!(edit_url));
52         }
53 
54         let content = ch.content.clone();
55         let content = utils::render_markdown(&content, ctx.html_config.curly_quotes);
56 
57         let fixed_content =
58             utils::render_markdown_with_path(&ch.content, ctx.html_config.curly_quotes, Some(path));
59         if !ctx.is_index {
60             // Add page break between chapters
61             // See https://developer.mozilla.org/en-US/docs/Web/CSS/break-before and https://developer.mozilla.org/en-US/docs/Web/CSS/page-break-before
62             // Add both two CSS properties because of the compatibility issue
63             print_content
64                 .push_str(r#"<div style="break-before: page; page-break-before: always;"></div>"#);
65         }
66         print_content.push_str(&fixed_content);
67 
68         // Update the context with data for this file
69         let ctx_path = path
70             .to_str()
71             .with_context(|| "Could not convert path to str")?;
72         let filepath = Path::new(&ctx_path).with_extension("html");
73 
74         // "print.html" is used for the print page.
75         if path == Path::new("print.md") {
76             bail!("{} is reserved for internal use", path.display());
77         };
78 
79         let book_title = ctx
80             .data
81             .get("book_title")
82             .and_then(serde_json::Value::as_str)
83             .unwrap_or("");
84 
85         let title = if let Some(title) = ctx.chapter_titles.get(path) {
86             title.clone()
87         } else if book_title.is_empty() {
88             ch.name.clone()
89         } else {
90             ch.name.clone() + " - " + book_title
91         };
92 
93         ctx.data.insert("path".to_owned(), json!(path));
94         ctx.data.insert("content".to_owned(), json!(content));
95         ctx.data.insert("chapter_title".to_owned(), json!(ch.name));
96         ctx.data.insert("title".to_owned(), json!(title));
97         ctx.data.insert(
98             "path_to_root".to_owned(),
99             json!(utils::fs::path_to_root(&path)),
100         );
101         if let Some(ref section) = ch.number {
102             ctx.data
103                 .insert("section".to_owned(), json!(section.to_string()));
104         }
105 
106         // Render the handlebars template with the data
107         debug!("Render template");
108         let rendered = ctx.handlebars.render("index", &ctx.data)?;
109 
110         let rendered = self.post_process(rendered, &ctx.html_config.playground, ctx.edition);
111 
112         // Write to file
113         debug!("Creating {}", filepath.display());
114         utils::fs::write_file(&ctx.destination, &filepath, rendered.as_bytes())?;
115 
116         if ctx.is_index {
117             ctx.data.insert("path".to_owned(), json!("index.md"));
118             ctx.data.insert("path_to_root".to_owned(), json!(""));
119             ctx.data.insert("is_index".to_owned(), json!("true"));
120             let rendered_index = ctx.handlebars.render("index", &ctx.data)?;
121             let rendered_index =
122                 self.post_process(rendered_index, &ctx.html_config.playground, ctx.edition);
123             debug!("Creating index.html from {}", ctx_path);
124             utils::fs::write_file(&ctx.destination, "index.html", rendered_index.as_bytes())?;
125         }
126 
127         Ok(())
128     }
129 
render_404( &self, ctx: &RenderContext, html_config: &HtmlConfig, src_dir: &Path, handlebars: &mut Handlebars<'_>, data: &mut serde_json::Map<String, serde_json::Value>, ) -> Result<()>130     fn render_404(
131         &self,
132         ctx: &RenderContext,
133         html_config: &HtmlConfig,
134         src_dir: &Path,
135         handlebars: &mut Handlebars<'_>,
136         data: &mut serde_json::Map<String, serde_json::Value>,
137     ) -> Result<()> {
138         let destination = &ctx.destination;
139         let content_404 = if let Some(ref filename) = html_config.input_404 {
140             let path = src_dir.join(filename);
141             std::fs::read_to_string(&path)
142                 .with_context(|| format!("unable to open 404 input file {:?}", path))?
143         } else {
144             // 404 input not explicitly configured try the default file 404.md
145             let default_404_location = src_dir.join("404.md");
146             if default_404_location.exists() {
147                 std::fs::read_to_string(&default_404_location).with_context(|| {
148                     format!("unable to open 404 input file {:?}", default_404_location)
149                 })?
150             } else {
151                 "# Document not found (404)\n\nThis URL is invalid, sorry. Please use the \
152                 navigation bar or search to continue."
153                     .to_string()
154             }
155         };
156         let html_content_404 = utils::render_markdown(&content_404, html_config.curly_quotes);
157 
158         let mut data_404 = data.clone();
159         let base_url = if let Some(site_url) = &html_config.site_url {
160             site_url
161         } else {
162             debug!(
163                 "HTML 'site-url' parameter not set, defaulting to '/'. Please configure \
164                 this to ensure the 404 page work correctly, especially if your site is hosted in a \
165                 subdirectory on the HTTP server."
166             );
167             "/"
168         };
169         data_404.insert("base_url".to_owned(), json!(base_url));
170         // Set a dummy path to ensure other paths (e.g. in the TOC) are generated correctly
171         data_404.insert("path".to_owned(), json!("404.md"));
172         data_404.insert("content".to_owned(), json!(html_content_404));
173         let rendered = handlebars.render("index", &data_404)?;
174 
175         let rendered =
176             self.post_process(rendered, &html_config.playground, ctx.config.rust.edition);
177         let output_file = get_404_output_file(&html_config.input_404);
178         utils::fs::write_file(destination, output_file, rendered.as_bytes())?;
179         debug!("Creating 404.html ✓");
180         Ok(())
181     }
182 
183     #[cfg_attr(feature = "cargo-clippy", allow(clippy::let_and_return))]
post_process( &self, rendered: String, playground_config: &Playground, edition: Option<RustEdition>, ) -> String184     fn post_process(
185         &self,
186         rendered: String,
187         playground_config: &Playground,
188         edition: Option<RustEdition>,
189     ) -> String {
190         let rendered = build_header_links(&rendered);
191         let rendered = fix_code_blocks(&rendered);
192         let rendered = add_playground_pre(&rendered, playground_config, edition);
193 
194         rendered
195     }
196 
copy_static_files( &self, destination: &Path, theme: &Theme, html_config: &HtmlConfig, ) -> Result<()>197     fn copy_static_files(
198         &self,
199         destination: &Path,
200         theme: &Theme,
201         html_config: &HtmlConfig,
202     ) -> Result<()> {
203         use crate::utils::fs::write_file;
204 
205         write_file(
206             destination,
207             ".nojekyll",
208             b"This file makes sure that Github Pages doesn't process mdBook's output.\n",
209         )?;
210 
211         if let Some(cname) = &html_config.cname {
212             write_file(destination, "CNAME", format!("{}\n", cname).as_bytes())?;
213         }
214 
215         write_file(destination, "book.js", &theme.js)?;
216         write_file(destination, "css/general.css", &theme.general_css)?;
217         write_file(destination, "css/chrome.css", &theme.chrome_css)?;
218         if html_config.print.enable {
219             write_file(destination, "css/print.css", &theme.print_css)?;
220         }
221         write_file(destination, "css/variables.css", &theme.variables_css)?;
222         if let Some(contents) = &theme.favicon_png {
223             write_file(destination, "favicon.png", contents)?;
224         }
225         if let Some(contents) = &theme.favicon_svg {
226             write_file(destination, "favicon.svg", contents)?;
227         }
228         write_file(destination, "highlight.css", &theme.highlight_css)?;
229         write_file(destination, "tomorrow-night.css", &theme.tomorrow_night_css)?;
230         write_file(destination, "ayu-highlight.css", &theme.ayu_highlight_css)?;
231         write_file(destination, "highlight.js", &theme.highlight_js)?;
232         write_file(destination, "clipboard.min.js", &theme.clipboard_js)?;
233         write_file(
234             destination,
235             "FontAwesome/css/font-awesome.css",
236             theme::FONT_AWESOME,
237         )?;
238         write_file(
239             destination,
240             "FontAwesome/fonts/fontawesome-webfont.eot",
241             theme::FONT_AWESOME_EOT,
242         )?;
243         write_file(
244             destination,
245             "FontAwesome/fonts/fontawesome-webfont.svg",
246             theme::FONT_AWESOME_SVG,
247         )?;
248         write_file(
249             destination,
250             "FontAwesome/fonts/fontawesome-webfont.ttf",
251             theme::FONT_AWESOME_TTF,
252         )?;
253         write_file(
254             destination,
255             "FontAwesome/fonts/fontawesome-webfont.woff",
256             theme::FONT_AWESOME_WOFF,
257         )?;
258         write_file(
259             destination,
260             "FontAwesome/fonts/fontawesome-webfont.woff2",
261             theme::FONT_AWESOME_WOFF2,
262         )?;
263         write_file(
264             destination,
265             "FontAwesome/fonts/FontAwesome.ttf",
266             theme::FONT_AWESOME_TTF,
267         )?;
268         if html_config.copy_fonts {
269             write_file(destination, "fonts/fonts.css", theme::fonts::CSS)?;
270             for (file_name, contents) in theme::fonts::LICENSES.iter() {
271                 write_file(destination, file_name, contents)?;
272             }
273             for (file_name, contents) in theme::fonts::OPEN_SANS.iter() {
274                 write_file(destination, file_name, contents)?;
275             }
276             write_file(
277                 destination,
278                 theme::fonts::SOURCE_CODE_PRO.0,
279                 theme::fonts::SOURCE_CODE_PRO.1,
280             )?;
281         }
282 
283         let playground_config = &html_config.playground;
284 
285         // Ace is a very large dependency, so only load it when requested
286         if playground_config.editable && playground_config.copy_js {
287             // Load the editor
288             write_file(destination, "editor.js", playground_editor::JS)?;
289             write_file(destination, "ace.js", playground_editor::ACE_JS)?;
290             write_file(destination, "mode-rust.js", playground_editor::MODE_RUST_JS)?;
291             write_file(
292                 destination,
293                 "theme-dawn.js",
294                 playground_editor::THEME_DAWN_JS,
295             )?;
296             write_file(
297                 destination,
298                 "theme-tomorrow_night.js",
299                 playground_editor::THEME_TOMORROW_NIGHT_JS,
300             )?;
301         }
302 
303         Ok(())
304     }
305 
306     /// Update the context with data for this file
configure_print_version( &self, data: &mut serde_json::Map<String, serde_json::Value>, print_content: &str, )307     fn configure_print_version(
308         &self,
309         data: &mut serde_json::Map<String, serde_json::Value>,
310         print_content: &str,
311     ) {
312         // Make sure that the Print chapter does not display the title from
313         // the last rendered chapter by removing it from its context
314         data.remove("title");
315         data.insert("is_print".to_owned(), json!(true));
316         data.insert("path".to_owned(), json!("print.md"));
317         data.insert("content".to_owned(), json!(print_content));
318         data.insert(
319             "path_to_root".to_owned(),
320             json!(utils::fs::path_to_root(Path::new("print.md"))),
321         );
322     }
323 
register_hbs_helpers(&self, handlebars: &mut Handlebars<'_>, html_config: &HtmlConfig)324     fn register_hbs_helpers(&self, handlebars: &mut Handlebars<'_>, html_config: &HtmlConfig) {
325         handlebars.register_helper(
326             "toc",
327             Box::new(helpers::toc::RenderToc {
328                 no_section_label: html_config.no_section_label,
329             }),
330         );
331         handlebars.register_helper("previous", Box::new(helpers::navigation::previous));
332         handlebars.register_helper("next", Box::new(helpers::navigation::next));
333         handlebars.register_helper("theme_option", Box::new(helpers::theme::theme_option));
334     }
335 
336     /// Copy across any additional CSS and JavaScript files which the book
337     /// has been configured to use.
copy_additional_css_and_js( &self, html: &HtmlConfig, root: &Path, destination: &Path, ) -> Result<()>338     fn copy_additional_css_and_js(
339         &self,
340         html: &HtmlConfig,
341         root: &Path,
342         destination: &Path,
343     ) -> Result<()> {
344         let custom_files = html.additional_css.iter().chain(html.additional_js.iter());
345 
346         debug!("Copying additional CSS and JS");
347 
348         for custom_file in custom_files {
349             let input_location = root.join(custom_file);
350             let output_location = destination.join(custom_file);
351             if let Some(parent) = output_location.parent() {
352                 fs::create_dir_all(parent)
353                     .with_context(|| format!("Unable to create {}", parent.display()))?;
354             }
355             debug!(
356                 "Copying {} -> {}",
357                 input_location.display(),
358                 output_location.display()
359             );
360 
361             fs::copy(&input_location, &output_location).with_context(|| {
362                 format!(
363                     "Unable to copy {} to {}",
364                     input_location.display(),
365                     output_location.display()
366                 )
367             })?;
368         }
369 
370         Ok(())
371     }
372 
emit_redirects( &self, root: &Path, handlebars: &Handlebars<'_>, redirects: &HashMap<String, String>, ) -> Result<()>373     fn emit_redirects(
374         &self,
375         root: &Path,
376         handlebars: &Handlebars<'_>,
377         redirects: &HashMap<String, String>,
378     ) -> Result<()> {
379         if redirects.is_empty() {
380             return Ok(());
381         }
382 
383         log::debug!("Emitting redirects");
384 
385         for (original, new) in redirects {
386             log::debug!("Redirecting \"{}\" → \"{}\"", original, new);
387             // Note: all paths are relative to the build directory, so the
388             // leading slash in an absolute path means nothing (and would mess
389             // up `root.join(original)`).
390             let original = original.trim_start_matches('/');
391             let filename = root.join(original);
392             self.emit_redirect(handlebars, &filename, new)?;
393         }
394 
395         Ok(())
396     }
397 
emit_redirect( &self, handlebars: &Handlebars<'_>, original: &Path, destination: &str, ) -> Result<()>398     fn emit_redirect(
399         &self,
400         handlebars: &Handlebars<'_>,
401         original: &Path,
402         destination: &str,
403     ) -> Result<()> {
404         if original.exists() {
405             // sanity check to avoid accidentally overwriting a real file.
406             let msg = format!(
407                 "Not redirecting \"{}\" to \"{}\" because it already exists. Are you sure it needs to be redirected?",
408                 original.display(),
409                 destination,
410             );
411             return Err(Error::msg(msg));
412         }
413 
414         if let Some(parent) = original.parent() {
415             std::fs::create_dir_all(parent)
416                 .with_context(|| format!("Unable to ensure \"{}\" exists", parent.display()))?;
417         }
418 
419         let ctx = json!({
420             "url": destination,
421         });
422         let f = File::create(original)?;
423         handlebars
424             .render_to_write("redirect", &ctx, f)
425             .with_context(|| {
426                 format!(
427                     "Unable to create a redirect file at \"{}\"",
428                     original.display()
429                 )
430             })?;
431 
432         Ok(())
433     }
434 }
435 
436 // TODO(mattico): Remove some time after the 0.1.8 release
maybe_wrong_theme_dir(dir: &Path) -> Result<bool>437 fn maybe_wrong_theme_dir(dir: &Path) -> Result<bool> {
438     fn entry_is_maybe_book_file(entry: fs::DirEntry) -> Result<bool> {
439         Ok(entry.file_type()?.is_file()
440             && entry.path().extension().map_or(false, |ext| ext == "md"))
441     }
442 
443     if dir.is_dir() {
444         for entry in fs::read_dir(dir)? {
445             if entry_is_maybe_book_file(entry?).unwrap_or(false) {
446                 return Ok(false);
447             }
448         }
449         Ok(true)
450     } else {
451         Ok(false)
452     }
453 }
454 
455 impl Renderer for HtmlHandlebars {
name(&self) -> &str456     fn name(&self) -> &str {
457         "html"
458     }
459 
render(&self, ctx: &RenderContext) -> Result<()>460     fn render(&self, ctx: &RenderContext) -> Result<()> {
461         let book_config = &ctx.config.book;
462         let html_config = ctx.config.html_config().unwrap_or_default();
463         let src_dir = ctx.root.join(&ctx.config.book.src);
464         let destination = &ctx.destination;
465         let book = &ctx.book;
466         let build_dir = ctx.root.join(&ctx.config.build.build_dir);
467 
468         if destination.exists() {
469             utils::fs::remove_dir_content(destination)
470                 .with_context(|| "Unable to remove stale HTML output")?;
471         }
472 
473         trace!("render");
474         let mut handlebars = Handlebars::new();
475 
476         let theme_dir = match html_config.theme {
477             Some(ref theme) => ctx.root.join(theme),
478             None => ctx.root.join("theme"),
479         };
480 
481         if html_config.theme.is_none()
482             && maybe_wrong_theme_dir(&src_dir.join("theme")).unwrap_or(false)
483         {
484             warn!(
485                 "Previous versions of mdBook erroneously accepted `./src/theme` as an automatic \
486                  theme directory"
487             );
488             warn!("Please move your theme files to `./theme` for them to continue being used");
489         }
490 
491         let theme = theme::Theme::new(theme_dir);
492 
493         debug!("Register the index handlebars template");
494         handlebars.register_template_string("index", String::from_utf8(theme.index.clone())?)?;
495 
496         debug!("Register the head handlebars template");
497         handlebars.register_partial("head", String::from_utf8(theme.head.clone())?)?;
498 
499         debug!("Register the redirect handlebars template");
500         handlebars
501             .register_template_string("redirect", String::from_utf8(theme.redirect.clone())?)?;
502 
503         debug!("Register the header handlebars template");
504         handlebars.register_partial("header", String::from_utf8(theme.header.clone())?)?;
505 
506         debug!("Register handlebars helpers");
507         self.register_hbs_helpers(&mut handlebars, &html_config);
508 
509         let mut data = make_data(&ctx.root, book, &ctx.config, &html_config, &theme)?;
510 
511         // Print version
512         let mut print_content = String::new();
513 
514         fs::create_dir_all(&destination)
515             .with_context(|| "Unexpected error when constructing destination path")?;
516 
517         let mut is_index = true;
518         for item in book.iter() {
519             let ctx = RenderItemContext {
520                 handlebars: &handlebars,
521                 destination: destination.to_path_buf(),
522                 data: data.clone(),
523                 is_index,
524                 book_config: book_config.clone(),
525                 html_config: html_config.clone(),
526                 edition: ctx.config.rust.edition,
527                 chapter_titles: &ctx.chapter_titles,
528             };
529             self.render_item(item, ctx, &mut print_content)?;
530             is_index = false;
531         }
532 
533         // Render 404 page
534         if html_config.input_404 != Some("".to_string()) {
535             self.render_404(ctx, &html_config, &src_dir, &mut handlebars, &mut data)?;
536         }
537 
538         // Print version
539         self.configure_print_version(&mut data, &print_content);
540         if let Some(ref title) = ctx.config.book.title {
541             data.insert("title".to_owned(), json!(title));
542         }
543 
544         // Render the handlebars template with the data
545         if html_config.print.enable {
546             debug!("Render template");
547             let rendered = handlebars.render("index", &data)?;
548 
549             let rendered =
550                 self.post_process(rendered, &html_config.playground, ctx.config.rust.edition);
551 
552             utils::fs::write_file(destination, "print.html", rendered.as_bytes())?;
553             debug!("Creating print.html ✓");
554         }
555 
556         debug!("Copy static files");
557         self.copy_static_files(destination, &theme, &html_config)
558             .with_context(|| "Unable to copy across static files")?;
559         self.copy_additional_css_and_js(&html_config, &ctx.root, destination)
560             .with_context(|| "Unable to copy across additional CSS and JS")?;
561 
562         // Render search index
563         #[cfg(feature = "search")]
564         {
565             let search = html_config.search.unwrap_or_default();
566             if search.enable {
567                 super::search::create_files(&search, destination, book)?;
568             }
569         }
570 
571         self.emit_redirects(&ctx.destination, &handlebars, &html_config.redirect)
572             .context("Unable to emit redirects")?;
573 
574         // Copy all remaining files, avoid a recursive copy from/to the book build dir
575         utils::fs::copy_files_except_ext(&src_dir, destination, true, Some(&build_dir), &["md"])?;
576 
577         Ok(())
578     }
579 }
580 
make_data( root: &Path, book: &Book, config: &Config, html_config: &HtmlConfig, theme: &Theme, ) -> Result<serde_json::Map<String, serde_json::Value>>581 fn make_data(
582     root: &Path,
583     book: &Book,
584     config: &Config,
585     html_config: &HtmlConfig,
586     theme: &Theme,
587 ) -> Result<serde_json::Map<String, serde_json::Value>> {
588     trace!("make_data");
589 
590     let mut data = serde_json::Map::new();
591     data.insert(
592         "language".to_owned(),
593         json!(config.book.language.clone().unwrap_or_default()),
594     );
595     data.insert(
596         "book_title".to_owned(),
597         json!(config.book.title.clone().unwrap_or_default()),
598     );
599     data.insert(
600         "description".to_owned(),
601         json!(config.book.description.clone().unwrap_or_default()),
602     );
603     if theme.favicon_png.is_some() {
604         data.insert("favicon_png".to_owned(), json!("favicon.png"));
605     }
606     if theme.favicon_svg.is_some() {
607         data.insert("favicon_svg".to_owned(), json!("favicon.svg"));
608     }
609     if let Some(ref livereload) = html_config.livereload_url {
610         data.insert("livereload".to_owned(), json!(livereload));
611     }
612 
613     let default_theme = match html_config.default_theme {
614         Some(ref theme) => theme.to_lowercase(),
615         None => "light".to_string(),
616     };
617     data.insert("default_theme".to_owned(), json!(default_theme));
618 
619     let preferred_dark_theme = match html_config.preferred_dark_theme {
620         Some(ref theme) => theme.to_lowercase(),
621         None => "navy".to_string(),
622     };
623     data.insert(
624         "preferred_dark_theme".to_owned(),
625         json!(preferred_dark_theme),
626     );
627 
628     // Add google analytics tag
629     if let Some(ref ga) = html_config.google_analytics {
630         data.insert("google_analytics".to_owned(), json!(ga));
631     }
632 
633     if html_config.mathjax_support {
634         data.insert("mathjax_support".to_owned(), json!(true));
635     }
636 
637     if html_config.copy_fonts {
638         data.insert("copy_fonts".to_owned(), json!(true));
639     }
640 
641     // Add check to see if there is an additional style
642     if !html_config.additional_css.is_empty() {
643         let mut css = Vec::new();
644         for style in &html_config.additional_css {
645             match style.strip_prefix(root) {
646                 Ok(p) => css.push(p.to_str().expect("Could not convert to str")),
647                 Err(_) => css.push(style.to_str().expect("Could not convert to str")),
648             }
649         }
650         data.insert("additional_css".to_owned(), json!(css));
651     }
652 
653     // Add check to see if there is an additional script
654     if !html_config.additional_js.is_empty() {
655         let mut js = Vec::new();
656         for script in &html_config.additional_js {
657             match script.strip_prefix(root) {
658                 Ok(p) => js.push(p.to_str().expect("Could not convert to str")),
659                 Err(_) => js.push(script.to_str().expect("Could not convert to str")),
660             }
661         }
662         data.insert("additional_js".to_owned(), json!(js));
663     }
664 
665     if html_config.playground.editable && html_config.playground.copy_js {
666         data.insert("playground_js".to_owned(), json!(true));
667         if html_config.playground.line_numbers {
668             data.insert("playground_line_numbers".to_owned(), json!(true));
669         }
670     }
671     if html_config.playground.copyable {
672         data.insert("playground_copyable".to_owned(), json!(true));
673     }
674 
675     data.insert("print_enable".to_owned(), json!(html_config.print.enable));
676     data.insert("fold_enable".to_owned(), json!(html_config.fold.enable));
677     data.insert("fold_level".to_owned(), json!(html_config.fold.level));
678 
679     let search = html_config.search.clone();
680     if cfg!(feature = "search") {
681         let search = search.unwrap_or_default();
682         data.insert("search_enabled".to_owned(), json!(search.enable));
683         data.insert(
684             "search_js".to_owned(),
685             json!(search.enable && search.copy_js),
686         );
687     } else if search.is_some() {
688         warn!("mdBook compiled without search support, ignoring `output.html.search` table");
689         warn!(
690             "please reinstall with `cargo install mdbook --force --features search`to use the \
691              search feature"
692         )
693     }
694 
695     if let Some(ref git_repository_url) = html_config.git_repository_url {
696         data.insert("git_repository_url".to_owned(), json!(git_repository_url));
697     }
698 
699     let git_repository_icon = match html_config.git_repository_icon {
700         Some(ref git_repository_icon) => git_repository_icon,
701         None => "fa-github",
702     };
703     data.insert("git_repository_icon".to_owned(), json!(git_repository_icon));
704 
705     let mut chapters = vec![];
706 
707     for item in book.iter() {
708         // Create the data to inject in the template
709         let mut chapter = BTreeMap::new();
710 
711         match *item {
712             BookItem::PartTitle(ref title) => {
713                 chapter.insert("part".to_owned(), json!(title));
714             }
715             BookItem::Chapter(ref ch) => {
716                 if let Some(ref section) = ch.number {
717                     chapter.insert("section".to_owned(), json!(section.to_string()));
718                 }
719 
720                 chapter.insert(
721                     "has_sub_items".to_owned(),
722                     json!((!ch.sub_items.is_empty()).to_string()),
723                 );
724 
725                 chapter.insert("name".to_owned(), json!(ch.name));
726                 if let Some(ref path) = ch.path {
727                     let p = path
728                         .to_str()
729                         .with_context(|| "Could not convert path to str")?;
730                     chapter.insert("path".to_owned(), json!(p));
731                 }
732             }
733             BookItem::Separator => {
734                 chapter.insert("spacer".to_owned(), json!("_spacer_"));
735             }
736         }
737 
738         chapters.push(chapter);
739     }
740 
741     data.insert("chapters".to_owned(), json!(chapters));
742 
743     debug!("[*]: JSON constructed");
744     Ok(data)
745 }
746 
747 /// Goes through the rendered HTML, making sure all header tags have
748 /// an anchor respectively so people can link to sections directly.
build_header_links(html: &str) -> String749 fn build_header_links(html: &str) -> String {
750     let regex = Regex::new(r"<h(\d)>(.*?)</h\d>").unwrap();
751     let mut id_counter = HashMap::new();
752 
753     regex
754         .replace_all(html, |caps: &Captures<'_>| {
755             let level = caps[1]
756                 .parse()
757                 .expect("Regex should ensure we only ever get numbers here");
758 
759             insert_link_into_header(level, &caps[2], &mut id_counter)
760         })
761         .into_owned()
762 }
763 
764 /// Insert a sinle link into a header, making sure each link gets its own
765 /// unique ID by appending an auto-incremented number (if necessary).
insert_link_into_header( level: usize, content: &str, id_counter: &mut HashMap<String, usize>, ) -> String766 fn insert_link_into_header(
767     level: usize,
768     content: &str,
769     id_counter: &mut HashMap<String, usize>,
770 ) -> String {
771     let raw_id = utils::id_from_content(content);
772 
773     let id_count = id_counter.entry(raw_id.clone()).or_insert(0);
774 
775     let id = match *id_count {
776         0 => raw_id,
777         other => format!("{}-{}", raw_id, other),
778     };
779 
780     *id_count += 1;
781 
782     format!(
783         r##"<h{level} id="{id}"><a class="header" href="#{id}">{text}</a></h{level}>"##,
784         level = level,
785         id = id,
786         text = content
787     )
788 }
789 
790 // The rust book uses annotations for rustdoc to test code snippets,
791 // like the following:
792 // ```rust,should_panic
793 // fn main() {
794 //     // Code here
795 // }
796 // ```
797 // This function replaces all commas by spaces in the code block classes
fix_code_blocks(html: &str) -> String798 fn fix_code_blocks(html: &str) -> String {
799     let regex = Regex::new(r##"<code([^>]+)class="([^"]+)"([^>]*)>"##).unwrap();
800     regex
801         .replace_all(html, |caps: &Captures<'_>| {
802             let before = &caps[1];
803             let classes = &caps[2].replace(",", " ");
804             let after = &caps[3];
805 
806             format!(
807                 r#"<code{before}class="{classes}"{after}>"#,
808                 before = before,
809                 classes = classes,
810                 after = after
811             )
812         })
813         .into_owned()
814 }
815 
add_playground_pre( html: &str, playground_config: &Playground, edition: Option<RustEdition>, ) -> String816 fn add_playground_pre(
817     html: &str,
818     playground_config: &Playground,
819     edition: Option<RustEdition>,
820 ) -> String {
821     let regex = Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap();
822     regex
823         .replace_all(html, |caps: &Captures<'_>| {
824             let text = &caps[1];
825             let classes = &caps[2];
826             let code = &caps[3];
827 
828             if classes.contains("language-rust") {
829                 if (!classes.contains("ignore")
830                     && !classes.contains("noplayground")
831                     && !classes.contains("noplaypen"))
832                     || classes.contains("mdbook-runnable")
833                 {
834                     let contains_e2015 = classes.contains("edition2015");
835                     let contains_e2018 = classes.contains("edition2018");
836                     let contains_e2021 = classes.contains("edition2021");
837                     let edition_class = if contains_e2015 || contains_e2018 || contains_e2021 {
838                         // the user forced edition, we should not overwrite it
839                         ""
840                     } else {
841                         match edition {
842                             Some(RustEdition::E2015) => " edition2015",
843                             Some(RustEdition::E2018) => " edition2018",
844                             Some(RustEdition::E2021) => " edition2021",
845                             None => "",
846                         }
847                     };
848 
849                     // wrap the contents in an external pre block
850                     format!(
851                         "<pre class=\"playground\"><code class=\"{}{}\">{}</code></pre>",
852                         classes,
853                         edition_class,
854                         {
855                             let content: Cow<'_, str> = if playground_config.editable
856                                 && classes.contains("editable")
857                                 || text.contains("fn main")
858                                 || text.contains("quick_main!")
859                             {
860                                 code.into()
861                             } else {
862                                 // we need to inject our own main
863                                 let (attrs, code) = partition_source(code);
864 
865                                 format!(
866                                     "\n# #![allow(unused)]\n{}#fn main() {{\n{}#}}",
867                                     attrs, code
868                                 )
869                                 .into()
870                             };
871                             hide_lines(&content)
872                         }
873                     )
874                 } else {
875                     format!("<code class=\"{}\">{}</code>", classes, hide_lines(code))
876                 }
877             } else {
878                 // not language-rust, so no-op
879                 text.to_owned()
880             }
881         })
882         .into_owned()
883 }
884 
885 lazy_static! {
886     static ref BORING_LINES_REGEX: Regex = Regex::new(r"^(\s*)#(.?)(.*)$").unwrap();
887 }
888 
hide_lines(content: &str) -> String889 fn hide_lines(content: &str) -> String {
890     let mut result = String::with_capacity(content.len());
891     for line in content.lines() {
892         if let Some(caps) = BORING_LINES_REGEX.captures(line) {
893             if &caps[2] == "#" {
894                 result += &caps[1];
895                 result += &caps[2];
896                 result += &caps[3];
897                 result += "\n";
898                 continue;
899             } else if &caps[2] != "!" && &caps[2] != "[" {
900                 result += "<span class=\"boring\">";
901                 result += &caps[1];
902                 if &caps[2] != " " {
903                     result += &caps[2];
904                 }
905                 result += &caps[3];
906                 result += "\n";
907                 result += "</span>";
908                 continue;
909             }
910         }
911         result += line;
912         result += "\n";
913     }
914     result
915 }
916 
partition_source(s: &str) -> (String, String)917 fn partition_source(s: &str) -> (String, String) {
918     let mut after_header = false;
919     let mut before = String::new();
920     let mut after = String::new();
921 
922     for line in s.lines() {
923         let trimline = line.trim();
924         let header = trimline.chars().all(char::is_whitespace) || trimline.starts_with("#![");
925         if !header || after_header {
926             after_header = true;
927             after.push_str(line);
928             after.push('\n');
929         } else {
930             before.push_str(line);
931             before.push('\n');
932         }
933     }
934 
935     (before, after)
936 }
937 
938 struct RenderItemContext<'a> {
939     handlebars: &'a Handlebars<'a>,
940     destination: PathBuf,
941     data: serde_json::Map<String, serde_json::Value>,
942     is_index: bool,
943     book_config: BookConfig,
944     html_config: HtmlConfig,
945     edition: Option<RustEdition>,
946     chapter_titles: &'a HashMap<PathBuf, String>,
947 }
948 
949 #[cfg(test)]
950 mod tests {
951     use super::*;
952 
953     #[test]
original_build_header_links()954     fn original_build_header_links() {
955         let inputs = vec![
956             (
957                 "blah blah <h1>Foo</h1>",
958                 r##"blah blah <h1 id="foo"><a class="header" href="#foo">Foo</a></h1>"##,
959             ),
960             (
961                 "<h1>Foo</h1>",
962                 r##"<h1 id="foo"><a class="header" href="#foo">Foo</a></h1>"##,
963             ),
964             (
965                 "<h3>Foo^bar</h3>",
966                 r##"<h3 id="foobar"><a class="header" href="#foobar">Foo^bar</a></h3>"##,
967             ),
968             (
969                 "<h4></h4>",
970                 r##"<h4 id=""><a class="header" href="#"></a></h4>"##,
971             ),
972             (
973                 "<h4><em>Hï</em></h4>",
974                 r##"<h4 id="hï"><a class="header" href="#hï"><em>Hï</em></a></h4>"##,
975             ),
976             (
977                 "<h1>Foo</h1><h3>Foo</h3>",
978                 r##"<h1 id="foo"><a class="header" href="#foo">Foo</a></h1><h3 id="foo-1"><a class="header" href="#foo-1">Foo</a></h3>"##,
979             ),
980         ];
981 
982         for (src, should_be) in inputs {
983             let got = build_header_links(src);
984             assert_eq!(got, should_be);
985         }
986     }
987 
988     #[test]
add_playground()989     fn add_playground() {
990         let inputs = [
991           ("<code class=\"language-rust\">x()</code>",
992            "<pre class=\"playground\"><code class=\"language-rust\">\n<span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
993           ("<code class=\"language-rust\">fn main() {}</code>",
994            "<pre class=\"playground\"><code class=\"language-rust\">fn main() {}\n</code></pre>"),
995           ("<code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code>",
996            "<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";\n</code></pre>"),
997           ("<code class=\"language-rust editable\">let s = \"foo\n ## bar\n\";</code>",
998            "<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n\";\n</code></pre>"),
999           ("<code class=\"language-rust editable\">let s = \"foo\n # bar\n#\n\";</code>",
1000            "<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span><span class=\"boring\">\n</span>\";\n</code></pre>"),
1001           ("<code class=\"language-rust ignore\">let s = \"foo\n # bar\n\";</code>",
1002            "<code class=\"language-rust ignore\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";\n</code>"),
1003           ("<code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]</code>",
1004            "<pre class=\"playground\"><code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]\n</code></pre>"),
1005         ];
1006         for (src, should_be) in &inputs {
1007             let got = add_playground_pre(
1008                 src,
1009                 &Playground {
1010                     editable: true,
1011                     ..Playground::default()
1012                 },
1013                 None,
1014             );
1015             assert_eq!(&*got, *should_be);
1016         }
1017     }
1018     #[test]
add_playground_edition2015()1019     fn add_playground_edition2015() {
1020         let inputs = [
1021           ("<code class=\"language-rust\">x()</code>",
1022            "<pre class=\"playground\"><code class=\"language-rust edition2015\">\n<span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
1023           ("<code class=\"language-rust\">fn main() {}</code>",
1024            "<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
1025           ("<code class=\"language-rust edition2015\">fn main() {}</code>",
1026            "<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
1027           ("<code class=\"language-rust edition2018\">fn main() {}</code>",
1028            "<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
1029         ];
1030         for (src, should_be) in &inputs {
1031             let got = add_playground_pre(
1032                 src,
1033                 &Playground {
1034                     editable: true,
1035                     ..Playground::default()
1036                 },
1037                 Some(RustEdition::E2015),
1038             );
1039             assert_eq!(&*got, *should_be);
1040         }
1041     }
1042     #[test]
add_playground_edition2018()1043     fn add_playground_edition2018() {
1044         let inputs = [
1045           ("<code class=\"language-rust\">x()</code>",
1046            "<pre class=\"playground\"><code class=\"language-rust edition2018\">\n<span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
1047           ("<code class=\"language-rust\">fn main() {}</code>",
1048            "<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
1049           ("<code class=\"language-rust edition2015\">fn main() {}</code>",
1050            "<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
1051           ("<code class=\"language-rust edition2018\">fn main() {}</code>",
1052            "<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
1053         ];
1054         for (src, should_be) in &inputs {
1055             let got = add_playground_pre(
1056                 src,
1057                 &Playground {
1058                     editable: true,
1059                     ..Playground::default()
1060                 },
1061                 Some(RustEdition::E2018),
1062             );
1063             assert_eq!(&*got, *should_be);
1064         }
1065     }
1066     #[test]
add_playground_edition2021()1067     fn add_playground_edition2021() {
1068         let inputs = [
1069             ("<code class=\"language-rust\">x()</code>",
1070              "<pre class=\"playground\"><code class=\"language-rust edition2021\">\n<span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
1071             ("<code class=\"language-rust\">fn main() {}</code>",
1072              "<pre class=\"playground\"><code class=\"language-rust edition2021\">fn main() {}\n</code></pre>"),
1073             ("<code class=\"language-rust edition2015\">fn main() {}</code>",
1074              "<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
1075             ("<code class=\"language-rust edition2018\">fn main() {}</code>",
1076              "<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
1077         ];
1078         for (src, should_be) in &inputs {
1079             let got = add_playground_pre(
1080                 src,
1081                 &Playground {
1082                     editable: true,
1083                     ..Playground::default()
1084                 },
1085                 Some(RustEdition::E2021),
1086             );
1087             assert_eq!(&*got, *should_be);
1088         }
1089     }
1090 }
1091