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