]> git.proxmox.com Git - rustc.git/blob - 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
1 use crate::book::{Book, BookItem};
2 use crate::config::{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 {
23 pub fn new() -> Self {
24 HtmlHandlebars
25 }
26
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 let content = ch.content.clone();
41 let content = utils::render_markdown(&content, ctx.html_config.curly_quotes);
42
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");
55
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 };
60
61 let book_title = ctx
62 .data
63 .get("book_title")
64 .and_then(serde_json::Value::as_str)
65 .unwrap_or("");
66
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())?;
104 }
105
106 Ok(())
107 }
108
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
162 #[cfg_attr(feature = "cargo-clippy", allow(clippy::let_and_return))]
163 fn post_process(
164 &self,
165 rendered: String,
166 playground_config: &Playground,
167 edition: Option<RustEdition>,
168 ) -> String {
169 let rendered = build_header_links(&rendered);
170 let rendered = fix_code_blocks(&rendered);
171 let rendered = add_playground_pre(&rendered, playground_config, edition);
172
173 rendered
174 }
175
176 fn copy_static_files(
177 &self,
178 destination: &Path,
179 theme: &Theme,
180 html_config: &HtmlConfig,
181 ) -> Result<()> {
182 use crate::utils::fs::write_file;
183
184 write_file(
185 destination,
186 ".nojekyll",
187 b"This file makes sure that Github Pages doesn't process mdBook's output.\n",
188 )?;
189
190 if let Some(cname) = &html_config.cname {
191 write_file(destination, "CNAME", format!("{}\n", cname).as_bytes())?;
192 }
193
194 write_file(destination, "book.js", &theme.js)?;
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)?;
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 }
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(
211 destination,
212 "FontAwesome/css/font-awesome.css",
213 theme::FONT_AWESOME,
214 )?;
215 write_file(
216 destination,
217 "FontAwesome/fonts/fontawesome-webfont.eot",
218 theme::FONT_AWESOME_EOT,
219 )?;
220 write_file(
221 destination,
222 "FontAwesome/fonts/fontawesome-webfont.svg",
223 theme::FONT_AWESOME_SVG,
224 )?;
225 write_file(
226 destination,
227 "FontAwesome/fonts/fontawesome-webfont.ttf",
228 theme::FONT_AWESOME_TTF,
229 )?;
230 write_file(
231 destination,
232 "FontAwesome/fonts/fontawesome-webfont.woff",
233 theme::FONT_AWESOME_WOFF,
234 )?;
235 write_file(
236 destination,
237 "FontAwesome/fonts/fontawesome-webfont.woff2",
238 theme::FONT_AWESOME_WOFF2,
239 )?;
240 write_file(
241 destination,
242 "FontAwesome/fonts/FontAwesome.ttf",
243 theme::FONT_AWESOME_TTF,
244 )?;
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 }
259
260 let playground_config = &html_config.playground;
261
262 // Ace is a very large dependency, so only load it when requested
263 if playground_config.editable && playground_config.copy_js {
264 // Load the editor
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 )?;
273 write_file(
274 destination,
275 "theme-tomorrow_night.js",
276 playground_editor::THEME_TOMORROW_NIGHT_JS,
277 )?;
278 }
279
280 Ok(())
281 }
282
283 /// Update the context with data for this file
284 fn configure_print_version(
285 &self,
286 data: &mut serde_json::Map<String, serde_json::Value>,
287 print_content: &str,
288 ) {
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));
295 data.insert(
296 "path_to_root".to_owned(),
297 json!(utils::fs::path_to_root(Path::new("print.md"))),
298 );
299 }
300
301 fn register_hbs_helpers(&self, handlebars: &mut Handlebars<'_>, html_config: &HtmlConfig) {
302 handlebars.register_helper(
303 "toc",
304 Box::new(helpers::toc::RenderToc {
305 no_section_label: html_config.no_section_label,
306 }),
307 );
308 handlebars.register_helper("previous", Box::new(helpers::navigation::previous));
309 handlebars.register_helper("next", Box::new(helpers::navigation::next));
310 handlebars.register_helper("theme_option", Box::new(helpers::theme::theme_option));
311 }
312
313 /// Copy across any additional CSS and JavaScript files which the book
314 /// has been configured to use.
315 fn copy_additional_css_and_js(
316 &self,
317 html: &HtmlConfig,
318 root: &Path,
319 destination: &Path,
320 ) -> Result<()> {
321 let custom_files = html.additional_css.iter().chain(html.additional_js.iter());
322
323 debug!("Copying additional CSS and JS");
324
325 for custom_file in custom_files {
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)
330 .with_context(|| format!("Unable to create {}", parent.display()))?;
331 }
332 debug!(
333 "Copying {} -> {}",
334 input_location.display(),
335 output_location.display()
336 );
337
338 fs::copy(&input_location, &output_location).with_context(|| {
339 format!(
340 "Unable to copy {} to {}",
341 input_location.display(),
342 output_location.display()
343 )
344 })?;
345 }
346
347 Ok(())
348 }
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 }
411 }
412
413 // TODO(mattico): Remove some time after the 0.1.8 release
414 fn 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
432 impl Renderer for HtmlHandlebars {
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;
442 let build_dir = ctx.root.join(&ctx.config.build.build_dir);
443
444 if destination.exists() {
445 utils::fs::remove_dir_content(destination)
446 .with_context(|| "Unable to remove stale HTML output")?;
447 }
448
449 trace!("render");
450 let mut handlebars = Handlebars::new();
451
452 let theme_dir = match html_config.theme {
453 Some(ref theme) => theme.to_path_buf(),
454 None => ctx.root.join("theme"),
455 };
456
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
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())?)?;
471
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
479 debug!("Register the header handlebars template");
480 handlebars.register_partial("header", String::from_utf8(theme.header.clone())?)?;
481
482 debug!("Register handlebars helpers");
483 self.register_hbs_helpers(&mut handlebars, &html_config);
484
485 let mut data = make_data(&ctx.root, &book, &ctx.config, &html_config, &theme)?;
486
487 // Print version
488 let mut print_content = String::new();
489
490 fs::create_dir_all(&destination)
491 .with_context(|| "Unexpected error when constructing destination path")?;
492
493 let mut is_index = true;
494 for item in book.iter() {
495 let ctx = RenderItemContext {
496 handlebars: &handlebars,
497 destination: destination.to_path_buf(),
498 data: data.clone(),
499 is_index,
500 html_config: html_config.clone(),
501 edition: ctx.config.rust.edition,
502 };
503 self.render_item(item, ctx, &mut print_content)?;
504 is_index = false;
505 }
506
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
512 // Print version
513 self.configure_print_version(&mut data, &print_content);
514 if let Some(ref title) = ctx.config.book.title {
515 data.insert("title".to_owned(), json!(title));
516 }
517
518 // Render the handlebars template with the data
519 debug!("Render template");
520 let rendered = handlebars.render("index", &data)?;
521
522 let rendered =
523 self.post_process(rendered, &html_config.playground, ctx.config.rust.edition);
524
525 utils::fs::write_file(&destination, "print.html", rendered.as_bytes())?;
526 debug!("Creating print.html ✓");
527
528 debug!("Copy static files");
529 self.copy_static_files(&destination, &theme, &html_config)
530 .with_context(|| "Unable to copy across static files")?;
531 self.copy_additional_css_and_js(&html_config, &ctx.root, &destination)
532 .with_context(|| "Unable to copy across additional CSS and JS")?;
533
534 // Render search index
535 #[cfg(feature = "search")]
536 {
537 let search = html_config.search.unwrap_or_default();
538 if search.enable {
539 super::search::create_files(&search, &destination, &book)?;
540 }
541 }
542
543 self.emit_redirects(&ctx.destination, &handlebars, &html_config.redirect)
544 .context("Unable to emit redirects")?;
545
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"])?;
548
549 Ok(())
550 }
551 }
552
553 fn make_data(
554 root: &Path,
555 book: &Book,
556 config: &Config,
557 html_config: &HtmlConfig,
558 theme: &Theme,
559 ) -> Result<serde_json::Map<String, serde_json::Value>> {
560 trace!("make_data");
561
562 let mut data = serde_json::Map::new();
563 data.insert(
564 "language".to_owned(),
565 json!(config.book.language.clone().unwrap_or_default()),
566 );
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 );
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 }
581 if let Some(ref livereload) = html_config.livereload_url {
582 data.insert("livereload".to_owned(), json!(livereload));
583 }
584
585 let default_theme = match html_config.default_theme {
586 Some(ref theme) => theme.to_lowercase(),
587 None => "light".to_string(),
588 };
589 data.insert("default_theme".to_owned(), json!(default_theme));
590
591 let preferred_dark_theme = match html_config.preferred_dark_theme {
592 Some(ref theme) => theme.to_lowercase(),
593 None => "navy".to_string(),
594 };
595 data.insert(
596 "preferred_dark_theme".to_owned(),
597 json!(preferred_dark_theme),
598 );
599
600 // Add google analytics tag
601 if let Some(ref ga) = html_config.google_analytics {
602 data.insert("google_analytics".to_owned(), json!(ga));
603 }
604
605 if html_config.mathjax_support {
606 data.insert("mathjax_support".to_owned(), json!(true));
607 }
608
609 if html_config.copy_fonts {
610 data.insert("copy_fonts".to_owned(), json!(true));
611 }
612
613 // Add check to see if there is an additional style
614 if !html_config.additional_css.is_empty() {
615 let mut css = Vec::new();
616 for style in &html_config.additional_css {
617 match style.strip_prefix(root) {
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")),
620 }
621 }
622 data.insert("additional_css".to_owned(), json!(css));
623 }
624
625 // Add check to see if there is an additional script
626 if !html_config.additional_js.is_empty() {
627 let mut js = Vec::new();
628 for script in &html_config.additional_js {
629 match script.strip_prefix(root) {
630 Ok(p) => js.push(p.to_str().expect("Could not convert to str")),
631 Err(_) => js.push(script.to_str().expect("Could not convert to str")),
632 }
633 }
634 data.insert("additional_js".to_owned(), json!(js));
635 }
636
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));
641 }
642 }
643 if html_config.playground.copyable {
644 data.insert("playground_copyable".to_owned(), json!(true));
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)));
649
650 let search = html_config.search.clone();
651 if cfg!(feature = "search") {
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 );
658 } else if search.is_some() {
659 warn!("mdBook compiled without search support, ignoring `output.html.search` table");
660 warn!(
661 "please reinstall with `cargo install mdbook --force --features search`to use the \
662 search feature"
663 )
664 }
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 }
669
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
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 {
683 BookItem::PartTitle(ref title) => {
684 chapter.insert("part".to_owned(), json!(title));
685 }
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
691 chapter.insert(
692 "has_sub_items".to_owned(),
693 json!((!ch.sub_items.is_empty()).to_string()),
694 );
695
696 chapter.insert("name".to_owned(), json!(ch.name));
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 }
703 }
704 BookItem::Separator => {
705 chapter.insert("spacer".to_owned(), json!("_spacer_"));
706 }
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
718 /// Goes through the rendered HTML, making sure all header tags have
719 /// an anchor respectively so people can link to sections directly.
720 fn build_header_links(html: &str) -> String {
721 let regex = Regex::new(r"<h(\d)>(.*?)</h\d>").unwrap();
722 let mut id_counter = HashMap::new();
723
724 regex
725 .replace_all(html, |caps: &Captures<'_>| {
726 let level = caps[1]
727 .parse()
728 .expect("Regex should ensure we only ever get numbers here");
729
730 insert_link_into_header(level, &caps[2], &mut id_counter)
731 })
732 .into_owned()
733 }
734
735 /// Insert a sinle link into a header, making sure each link gets its own
736 /// unique ID by appending an auto-incremented number (if necessary).
737 fn insert_link_into_header(
738 level: usize,
739 content: &str,
740 id_counter: &mut HashMap<String, usize>,
741 ) -> String {
742 let raw_id = utils::id_from_content(content);
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!(
754 r##"<h{level}><a class="header" href="#{id}" id="{id}">{text}</a></h{level}>"##,
755 level = level,
756 id = id,
757 text = content
758 )
759 }
760
761 // The rust book uses annotations for rustdoc to test code snippets,
762 // like the following:
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
769 fn fix_code_blocks(html: &str) -> String {
770 let regex = Regex::new(r##"<code([^>]+)class="([^"]+)"([^>]*)>"##).unwrap();
771 regex
772 .replace_all(html, |caps: &Captures<'_>| {
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}>"#,
779 before = before,
780 classes = classes,
781 after = after
782 )
783 })
784 .into_owned()
785 }
786
787 fn add_playground_pre(
788 html: &str,
789 playground_config: &Playground,
790 edition: Option<RustEdition>,
791 ) -> String {
792 let regex = Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap();
793 regex
794 .replace_all(html, |caps: &Captures<'_>| {
795 let text = &caps[1];
796 let classes = &caps[2];
797 let code = &caps[3];
798
799 if classes.contains("language-rust") {
800 if (!classes.contains("ignore")
801 && !classes.contains("noplayground")
802 && !classes.contains("noplaypen"))
803 || classes.contains("mdbook-runnable")
804 {
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
818 // wrap the contents in an external pre block
819 format!(
820 "<pre class=\"playground\"><code class=\"{}{}\">{}</code></pre>",
821 classes,
822 edition_class,
823 {
824 let content: Cow<'_, str> = if playground_config.editable
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!(
835 "\n# #![allow(unused)]\n{}#fn main() {{\n{}#}}",
836 attrs, code
837 )
838 .into()
839 };
840 hide_lines(&content)
841 }
842 )
843 } else {
844 format!("<code class=\"{}\">{}</code>", classes, hide_lines(code))
845 }
846 } else {
847 // not language-rust, so no-op
848 text.to_owned()
849 }
850 })
851 .into_owned()
852 }
853
854 lazy_static! {
855 static ref BORING_LINES_REGEX: Regex = Regex::new(r"^(\s*)#(.?)(.*)$").unwrap();
856 }
857
858 fn 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
886 fn 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();
893 let header = trimline.chars().all(char::is_whitespace) || trimline.starts_with("#![");
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)
905 }
906
907 struct RenderItemContext<'a> {
908 handlebars: &'a Handlebars<'a>,
909 destination: PathBuf,
910 data: serde_json::Map<String, serde_json::Value>,
911 is_index: bool,
912 html_config: HtmlConfig,
913 edition: Option<RustEdition>,
914 }
915
916 #[cfg(test)]
917 mod tests {
918 use super::*;
919
920 #[test]
921 fn original_build_header_links() {
922 let inputs = vec![
923 (
924 "blah blah <h1>Foo</h1>",
925 r##"blah blah <h1><a class="header" href="#foo" id="foo">Foo</a></h1>"##,
926 ),
927 (
928 "<h1>Foo</h1>",
929 r##"<h1><a class="header" href="#foo" id="foo">Foo</a></h1>"##,
930 ),
931 (
932 "<h3>Foo^bar</h3>",
933 r##"<h3><a class="header" href="#foobar" id="foobar">Foo^bar</a></h3>"##,
934 ),
935 (
936 "<h4></h4>",
937 r##"<h4><a class="header" href="#" id=""></a></h4>"##,
938 ),
939 (
940 "<h4><em>Hï</em></h4>",
941 r##"<h4><a class="header" href="#hï" id=""><em>Hï</em></a></h4>"##,
942 ),
943 (
944 "<h1>Foo</h1><h3>Foo</h3>",
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>"##,
946 ),
947 ];
948
949 for (src, should_be) in inputs {
950 let got = build_header_links(&src);
951 assert_eq!(got, should_be);
952 }
953 }
954
955 #[test]
956 fn add_playground() {
957 let inputs = [
958 ("<code class=\"language-rust\">x()</code>",
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>"),
960 ("<code class=\"language-rust\">fn main() {}</code>",
961 "<pre class=\"playground\"><code class=\"language-rust\">fn main() {}\n</code></pre>"),
962 ("<code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code>",
963 "<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";\n</code></pre>"),
964 ("<code class=\"language-rust editable\">let s = \"foo\n ## bar\n\";</code>",
965 "<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n\";\n</code></pre>"),
966 ("<code class=\"language-rust editable\">let s = \"foo\n # bar\n#\n\";</code>",
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>"),
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>",
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>"),
1020 ];
1021 for (src, should_be) in &inputs {
1022 let got = add_playground_pre(
1023 src,
1024 &Playground {
1025 editable: true,
1026 ..Playground::default()
1027 },
1028 Some(RustEdition::E2018),
1029 );
1030 assert_eq!(&*got, *should_be);
1031 }
1032 }
1033 }