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