From 698f8c2f01ea549d77d7dc3338a12e04c11057b9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 17 Apr 2024 14:02:58 +0200 Subject: Adding upstream version 1.64.0+dfsg1. Signed-off-by: Daniel Baumann --- .../src/renderer/html_handlebars/hbs_renderer.rs | 1106 ++++++++++++++++++++ .../src/renderer/html_handlebars/helpers/mod.rs | 3 + .../renderer/html_handlebars/helpers/navigation.rs | 290 +++++ .../src/renderer/html_handlebars/helpers/theme.rs | 28 + .../src/renderer/html_handlebars/helpers/toc.rs | 203 ++++ vendor/mdbook/src/renderer/html_handlebars/mod.rs | 9 + .../mdbook/src/renderer/html_handlebars/search.rs | 286 +++++ vendor/mdbook/src/renderer/markdown_renderer.rs | 52 + vendor/mdbook/src/renderer/mod.rs | 265 +++++ 9 files changed, 2242 insertions(+) create mode 100644 vendor/mdbook/src/renderer/html_handlebars/hbs_renderer.rs create mode 100644 vendor/mdbook/src/renderer/html_handlebars/helpers/mod.rs create mode 100644 vendor/mdbook/src/renderer/html_handlebars/helpers/navigation.rs create mode 100644 vendor/mdbook/src/renderer/html_handlebars/helpers/theme.rs create mode 100644 vendor/mdbook/src/renderer/html_handlebars/helpers/toc.rs create mode 100644 vendor/mdbook/src/renderer/html_handlebars/mod.rs create mode 100644 vendor/mdbook/src/renderer/html_handlebars/search.rs create mode 100644 vendor/mdbook/src/renderer/markdown_renderer.rs create mode 100644 vendor/mdbook/src/renderer/mod.rs (limited to 'vendor/mdbook/src/renderer') diff --git a/vendor/mdbook/src/renderer/html_handlebars/hbs_renderer.rs b/vendor/mdbook/src/renderer/html_handlebars/hbs_renderer.rs new file mode 100644 index 000000000..b933a359a --- /dev/null +++ b/vendor/mdbook/src/renderer/html_handlebars/hbs_renderer.rs @@ -0,0 +1,1106 @@ +use crate::book::{Book, BookItem}; +use crate::config::{BookConfig, Config, HtmlConfig, Playground, RustEdition}; +use crate::errors::*; +use crate::renderer::html_handlebars::helpers; +use crate::renderer::{RenderContext, Renderer}; +use crate::theme::{self, playground_editor, Theme}; +use crate::utils; + +use std::borrow::Cow; +use std::collections::BTreeMap; +use std::collections::HashMap; +use std::fs::{self, File}; +use std::path::{Path, PathBuf}; + +use crate::utils::fs::get_404_output_file; +use handlebars::Handlebars; +use regex::{Captures, Regex}; + +#[derive(Default)] +pub struct HtmlHandlebars; + +impl HtmlHandlebars { + pub fn new() -> Self { + HtmlHandlebars + } + + fn render_item( + &self, + item: &BookItem, + mut ctx: RenderItemContext<'_>, + print_content: &mut String, + ) -> Result<()> { + // FIXME: This should be made DRY-er and rely less on mutable state + + let (ch, path) = match item { + BookItem::Chapter(ch) if !ch.is_draft_chapter() => (ch, ch.path.as_ref().unwrap()), + _ => return Ok(()), + }; + + if let Some(ref edit_url_template) = ctx.html_config.edit_url_template { + let full_path = ctx.book_config.src.to_str().unwrap_or_default().to_owned() + + "/" + + ch.source_path + .clone() + .unwrap_or_default() + .to_str() + .unwrap_or_default(); + + let edit_url = edit_url_template.replace("{path}", &full_path); + ctx.data + .insert("git_repository_edit_url".to_owned(), json!(edit_url)); + } + + let content = ch.content.clone(); + let content = utils::render_markdown(&content, ctx.html_config.curly_quotes); + + let fixed_content = + utils::render_markdown_with_path(&ch.content, ctx.html_config.curly_quotes, Some(path)); + if !ctx.is_index && ctx.html_config.print.page_break { + // Add page break between chapters + // 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 + // Add both two CSS properties because of the compatibility issue + print_content + .push_str(r#"
"#); + } + print_content.push_str(&fixed_content); + + // Update the context with data for this file + let ctx_path = path + .to_str() + .with_context(|| "Could not convert path to str")?; + let filepath = Path::new(&ctx_path).with_extension("html"); + + // "print.html" is used for the print page. + if path == Path::new("print.md") { + bail!("{} is reserved for internal use", path.display()); + }; + + let book_title = ctx + .data + .get("book_title") + .and_then(serde_json::Value::as_str) + .unwrap_or(""); + + let title = if let Some(title) = ctx.chapter_titles.get(path) { + title.clone() + } else if book_title.is_empty() { + ch.name.clone() + } else { + ch.name.clone() + " - " + book_title + }; + + ctx.data.insert("path".to_owned(), json!(path)); + ctx.data.insert("content".to_owned(), json!(content)); + ctx.data.insert("chapter_title".to_owned(), json!(ch.name)); + ctx.data.insert("title".to_owned(), json!(title)); + ctx.data.insert( + "path_to_root".to_owned(), + json!(utils::fs::path_to_root(&path)), + ); + if let Some(ref section) = ch.number { + ctx.data + .insert("section".to_owned(), json!(section.to_string())); + } + + // Render the handlebars template with the data + debug!("Render template"); + let rendered = ctx.handlebars.render("index", &ctx.data)?; + + let rendered = self.post_process(rendered, &ctx.html_config.playground, ctx.edition); + + // Write to file + debug!("Creating {}", filepath.display()); + utils::fs::write_file(&ctx.destination, &filepath, rendered.as_bytes())?; + + if ctx.is_index { + ctx.data.insert("path".to_owned(), json!("index.md")); + ctx.data.insert("path_to_root".to_owned(), json!("")); + ctx.data.insert("is_index".to_owned(), json!(true)); + let rendered_index = ctx.handlebars.render("index", &ctx.data)?; + let rendered_index = + self.post_process(rendered_index, &ctx.html_config.playground, ctx.edition); + debug!("Creating index.html from {}", ctx_path); + utils::fs::write_file(&ctx.destination, "index.html", rendered_index.as_bytes())?; + } + + Ok(()) + } + + fn render_404( + &self, + ctx: &RenderContext, + html_config: &HtmlConfig, + src_dir: &Path, + handlebars: &mut Handlebars<'_>, + data: &mut serde_json::Map, + ) -> Result<()> { + let destination = &ctx.destination; + let content_404 = if let Some(ref filename) = html_config.input_404 { + let path = src_dir.join(filename); + std::fs::read_to_string(&path) + .with_context(|| format!("unable to open 404 input file {:?}", path))? + } else { + // 404 input not explicitly configured try the default file 404.md + let default_404_location = src_dir.join("404.md"); + if default_404_location.exists() { + std::fs::read_to_string(&default_404_location).with_context(|| { + format!("unable to open 404 input file {:?}", default_404_location) + })? + } else { + "# Document not found (404)\n\nThis URL is invalid, sorry. Please use the \ + navigation bar or search to continue." + .to_string() + } + }; + let html_content_404 = utils::render_markdown(&content_404, html_config.curly_quotes); + + let mut data_404 = data.clone(); + let base_url = if let Some(site_url) = &html_config.site_url { + site_url + } else { + debug!( + "HTML 'site-url' parameter not set, defaulting to '/'. Please configure \ + this to ensure the 404 page work correctly, especially if your site is hosted in a \ + subdirectory on the HTTP server." + ); + "/" + }; + data_404.insert("base_url".to_owned(), json!(base_url)); + // Set a dummy path to ensure other paths (e.g. in the TOC) are generated correctly + data_404.insert("path".to_owned(), json!("404.md")); + data_404.insert("content".to_owned(), json!(html_content_404)); + + let mut title = String::from("Page not found"); + if let Some(book_title) = &ctx.config.book.title { + title.push_str(" - "); + title.push_str(book_title); + } + data_404.insert("title".to_owned(), json!(title)); + let rendered = handlebars.render("index", &data_404)?; + + let rendered = + self.post_process(rendered, &html_config.playground, ctx.config.rust.edition); + let output_file = get_404_output_file(&html_config.input_404); + utils::fs::write_file(destination, output_file, rendered.as_bytes())?; + debug!("Creating 404.html ✓"); + Ok(()) + } + + #[cfg_attr(feature = "cargo-clippy", allow(clippy::let_and_return))] + fn post_process( + &self, + rendered: String, + playground_config: &Playground, + edition: Option, + ) -> String { + let rendered = build_header_links(&rendered); + let rendered = fix_code_blocks(&rendered); + let rendered = add_playground_pre(&rendered, playground_config, edition); + + rendered + } + + fn copy_static_files( + &self, + destination: &Path, + theme: &Theme, + html_config: &HtmlConfig, + ) -> Result<()> { + use crate::utils::fs::write_file; + + write_file( + destination, + ".nojekyll", + b"This file makes sure that Github Pages doesn't process mdBook's output.\n", + )?; + + if let Some(cname) = &html_config.cname { + write_file(destination, "CNAME", format!("{}\n", cname).as_bytes())?; + } + + write_file(destination, "book.js", &theme.js)?; + write_file(destination, "css/general.css", &theme.general_css)?; + write_file(destination, "css/chrome.css", &theme.chrome_css)?; + if html_config.print.enable { + write_file(destination, "css/print.css", &theme.print_css)?; + } + write_file(destination, "css/variables.css", &theme.variables_css)?; + if let Some(contents) = &theme.favicon_png { + write_file(destination, "favicon.png", contents)?; + } + if let Some(contents) = &theme.favicon_svg { + write_file(destination, "favicon.svg", contents)?; + } + write_file(destination, "highlight.css", &theme.highlight_css)?; + write_file(destination, "tomorrow-night.css", &theme.tomorrow_night_css)?; + write_file(destination, "ayu-highlight.css", &theme.ayu_highlight_css)?; + write_file(destination, "highlight.js", &theme.highlight_js)?; + write_file(destination, "clipboard.min.js", &theme.clipboard_js)?; + write_file( + destination, + "FontAwesome/css/font-awesome.css", + theme::FONT_AWESOME, + )?; + write_file( + destination, + "FontAwesome/fonts/fontawesome-webfont.eot", + theme::FONT_AWESOME_EOT, + )?; + write_file( + destination, + "FontAwesome/fonts/fontawesome-webfont.svg", + theme::FONT_AWESOME_SVG, + )?; + write_file( + destination, + "FontAwesome/fonts/fontawesome-webfont.ttf", + theme::FONT_AWESOME_TTF, + )?; + write_file( + destination, + "FontAwesome/fonts/fontawesome-webfont.woff", + theme::FONT_AWESOME_WOFF, + )?; + write_file( + destination, + "FontAwesome/fonts/fontawesome-webfont.woff2", + theme::FONT_AWESOME_WOFF2, + )?; + write_file( + destination, + "FontAwesome/fonts/FontAwesome.ttf", + theme::FONT_AWESOME_TTF, + )?; + if html_config.copy_fonts { + write_file(destination, "fonts/fonts.css", theme::fonts::CSS)?; + for (file_name, contents) in theme::fonts::LICENSES.iter() { + write_file(destination, file_name, contents)?; + } + for (file_name, contents) in theme::fonts::OPEN_SANS.iter() { + write_file(destination, file_name, contents)?; + } + write_file( + destination, + theme::fonts::SOURCE_CODE_PRO.0, + theme::fonts::SOURCE_CODE_PRO.1, + )?; + } + + let playground_config = &html_config.playground; + + // Ace is a very large dependency, so only load it when requested + if playground_config.editable && playground_config.copy_js { + // Load the editor + write_file(destination, "editor.js", playground_editor::JS)?; + write_file(destination, "ace.js", playground_editor::ACE_JS)?; + write_file(destination, "mode-rust.js", playground_editor::MODE_RUST_JS)?; + write_file( + destination, + "theme-dawn.js", + playground_editor::THEME_DAWN_JS, + )?; + write_file( + destination, + "theme-tomorrow_night.js", + playground_editor::THEME_TOMORROW_NIGHT_JS, + )?; + } + + Ok(()) + } + + /// Update the context with data for this file + fn configure_print_version( + &self, + data: &mut serde_json::Map, + print_content: &str, + ) { + // Make sure that the Print chapter does not display the title from + // the last rendered chapter by removing it from its context + data.remove("title"); + data.insert("is_print".to_owned(), json!(true)); + data.insert("path".to_owned(), json!("print.md")); + data.insert("content".to_owned(), json!(print_content)); + data.insert( + "path_to_root".to_owned(), + json!(utils::fs::path_to_root(Path::new("print.md"))), + ); + } + + fn register_hbs_helpers(&self, handlebars: &mut Handlebars<'_>, html_config: &HtmlConfig) { + handlebars.register_helper( + "toc", + Box::new(helpers::toc::RenderToc { + no_section_label: html_config.no_section_label, + }), + ); + handlebars.register_helper("previous", Box::new(helpers::navigation::previous)); + handlebars.register_helper("next", Box::new(helpers::navigation::next)); + handlebars.register_helper("theme_option", Box::new(helpers::theme::theme_option)); + } + + /// Copy across any additional CSS and JavaScript files which the book + /// has been configured to use. + fn copy_additional_css_and_js( + &self, + html: &HtmlConfig, + root: &Path, + destination: &Path, + ) -> Result<()> { + let custom_files = html.additional_css.iter().chain(html.additional_js.iter()); + + debug!("Copying additional CSS and JS"); + + for custom_file in custom_files { + let input_location = root.join(custom_file); + let output_location = destination.join(custom_file); + if let Some(parent) = output_location.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("Unable to create {}", parent.display()))?; + } + debug!( + "Copying {} -> {}", + input_location.display(), + output_location.display() + ); + + fs::copy(&input_location, &output_location).with_context(|| { + format!( + "Unable to copy {} to {}", + input_location.display(), + output_location.display() + ) + })?; + } + + Ok(()) + } + + fn emit_redirects( + &self, + root: &Path, + handlebars: &Handlebars<'_>, + redirects: &HashMap, + ) -> Result<()> { + if redirects.is_empty() { + return Ok(()); + } + + log::debug!("Emitting redirects"); + + for (original, new) in redirects { + log::debug!("Redirecting \"{}\" → \"{}\"", original, new); + // Note: all paths are relative to the build directory, so the + // leading slash in an absolute path means nothing (and would mess + // up `root.join(original)`). + let original = original.trim_start_matches('/'); + let filename = root.join(original); + self.emit_redirect(handlebars, &filename, new)?; + } + + Ok(()) + } + + fn emit_redirect( + &self, + handlebars: &Handlebars<'_>, + original: &Path, + destination: &str, + ) -> Result<()> { + if original.exists() { + // sanity check to avoid accidentally overwriting a real file. + let msg = format!( + "Not redirecting \"{}\" to \"{}\" because it already exists. Are you sure it needs to be redirected?", + original.display(), + destination, + ); + return Err(Error::msg(msg)); + } + + if let Some(parent) = original.parent() { + std::fs::create_dir_all(parent) + .with_context(|| format!("Unable to ensure \"{}\" exists", parent.display()))?; + } + + let ctx = json!({ + "url": destination, + }); + let f = File::create(original)?; + handlebars + .render_to_write("redirect", &ctx, f) + .with_context(|| { + format!( + "Unable to create a redirect file at \"{}\"", + original.display() + ) + })?; + + Ok(()) + } +} + +// TODO(mattico): Remove some time after the 0.1.8 release +fn maybe_wrong_theme_dir(dir: &Path) -> Result { + fn entry_is_maybe_book_file(entry: fs::DirEntry) -> Result { + Ok(entry.file_type()?.is_file() + && entry.path().extension().map_or(false, |ext| ext == "md")) + } + + if dir.is_dir() { + for entry in fs::read_dir(dir)? { + if entry_is_maybe_book_file(entry?).unwrap_or(false) { + return Ok(false); + } + } + Ok(true) + } else { + Ok(false) + } +} + +impl Renderer for HtmlHandlebars { + fn name(&self) -> &str { + "html" + } + + fn render(&self, ctx: &RenderContext) -> Result<()> { + let book_config = &ctx.config.book; + let html_config = ctx.config.html_config().unwrap_or_default(); + let src_dir = ctx.root.join(&ctx.config.book.src); + let destination = &ctx.destination; + let book = &ctx.book; + let build_dir = ctx.root.join(&ctx.config.build.build_dir); + + if destination.exists() { + utils::fs::remove_dir_content(destination) + .with_context(|| "Unable to remove stale HTML output")?; + } + + trace!("render"); + let mut handlebars = Handlebars::new(); + + let theme_dir = match html_config.theme { + Some(ref theme) => { + let dir = ctx.root.join(theme); + if !dir.is_dir() { + bail!("theme dir {} does not exist", dir.display()); + } + dir + } + None => ctx.root.join("theme"), + }; + + if html_config.theme.is_none() + && maybe_wrong_theme_dir(&src_dir.join("theme")).unwrap_or(false) + { + warn!( + "Previous versions of mdBook erroneously accepted `./src/theme` as an automatic \ + theme directory" + ); + warn!("Please move your theme files to `./theme` for them to continue being used"); + } + + let theme = theme::Theme::new(theme_dir); + + debug!("Register the index handlebars template"); + handlebars.register_template_string("index", String::from_utf8(theme.index.clone())?)?; + + debug!("Register the head handlebars template"); + handlebars.register_partial("head", String::from_utf8(theme.head.clone())?)?; + + debug!("Register the redirect handlebars template"); + handlebars + .register_template_string("redirect", String::from_utf8(theme.redirect.clone())?)?; + + debug!("Register the header handlebars template"); + handlebars.register_partial("header", String::from_utf8(theme.header.clone())?)?; + + debug!("Register handlebars helpers"); + self.register_hbs_helpers(&mut handlebars, &html_config); + + let mut data = make_data(&ctx.root, book, &ctx.config, &html_config, &theme)?; + + // Print version + let mut print_content = String::new(); + + fs::create_dir_all(&destination) + .with_context(|| "Unexpected error when constructing destination path")?; + + let mut is_index = true; + for item in book.iter() { + let ctx = RenderItemContext { + handlebars: &handlebars, + destination: destination.to_path_buf(), + data: data.clone(), + is_index, + book_config: book_config.clone(), + html_config: html_config.clone(), + edition: ctx.config.rust.edition, + chapter_titles: &ctx.chapter_titles, + }; + self.render_item(item, ctx, &mut print_content)?; + // Only the first non-draft chapter item should be treated as the "index" + is_index &= !matches!(item, BookItem::Chapter(ch) if !ch.is_draft_chapter()); + } + + // Render 404 page + if html_config.input_404 != Some("".to_string()) { + self.render_404(ctx, &html_config, &src_dir, &mut handlebars, &mut data)?; + } + + // Print version + self.configure_print_version(&mut data, &print_content); + if let Some(ref title) = ctx.config.book.title { + data.insert("title".to_owned(), json!(title)); + } + + // Render the handlebars template with the data + if html_config.print.enable { + debug!("Render template"); + let rendered = handlebars.render("index", &data)?; + + let rendered = + self.post_process(rendered, &html_config.playground, ctx.config.rust.edition); + + utils::fs::write_file(destination, "print.html", rendered.as_bytes())?; + debug!("Creating print.html ✓"); + } + + debug!("Copy static files"); + self.copy_static_files(destination, &theme, &html_config) + .with_context(|| "Unable to copy across static files")?; + self.copy_additional_css_and_js(&html_config, &ctx.root, destination) + .with_context(|| "Unable to copy across additional CSS and JS")?; + + // Render search index + #[cfg(feature = "search")] + { + let search = html_config.search.unwrap_or_default(); + if search.enable { + super::search::create_files(&search, destination, book)?; + } + } + + self.emit_redirects(&ctx.destination, &handlebars, &html_config.redirect) + .context("Unable to emit redirects")?; + + // Copy all remaining files, avoid a recursive copy from/to the book build dir + utils::fs::copy_files_except_ext(&src_dir, destination, true, Some(&build_dir), &["md"])?; + + Ok(()) + } +} + +fn make_data( + root: &Path, + book: &Book, + config: &Config, + html_config: &HtmlConfig, + theme: &Theme, +) -> Result> { + trace!("make_data"); + + let mut data = serde_json::Map::new(); + data.insert( + "language".to_owned(), + json!(config.book.language.clone().unwrap_or_default()), + ); + data.insert( + "book_title".to_owned(), + json!(config.book.title.clone().unwrap_or_default()), + ); + data.insert( + "description".to_owned(), + json!(config.book.description.clone().unwrap_or_default()), + ); + if theme.favicon_png.is_some() { + data.insert("favicon_png".to_owned(), json!("favicon.png")); + } + if theme.favicon_svg.is_some() { + data.insert("favicon_svg".to_owned(), json!("favicon.svg")); + } + if let Some(ref live_reload_endpoint) = html_config.live_reload_endpoint { + data.insert( + "live_reload_endpoint".to_owned(), + json!(live_reload_endpoint), + ); + } + + let default_theme = match html_config.default_theme { + Some(ref theme) => theme.to_lowercase(), + None => "light".to_string(), + }; + data.insert("default_theme".to_owned(), json!(default_theme)); + + let preferred_dark_theme = match html_config.preferred_dark_theme { + Some(ref theme) => theme.to_lowercase(), + None => "navy".to_string(), + }; + data.insert( + "preferred_dark_theme".to_owned(), + json!(preferred_dark_theme), + ); + + // Add google analytics tag + if let Some(ref ga) = html_config.google_analytics { + data.insert("google_analytics".to_owned(), json!(ga)); + } + + if html_config.mathjax_support { + data.insert("mathjax_support".to_owned(), json!(true)); + } + + if html_config.copy_fonts { + data.insert("copy_fonts".to_owned(), json!(true)); + } + + // Add check to see if there is an additional style + if !html_config.additional_css.is_empty() { + let mut css = Vec::new(); + for style in &html_config.additional_css { + match style.strip_prefix(root) { + Ok(p) => css.push(p.to_str().expect("Could not convert to str")), + Err(_) => css.push(style.to_str().expect("Could not convert to str")), + } + } + data.insert("additional_css".to_owned(), json!(css)); + } + + // Add check to see if there is an additional script + if !html_config.additional_js.is_empty() { + let mut js = Vec::new(); + for script in &html_config.additional_js { + match script.strip_prefix(root) { + Ok(p) => js.push(p.to_str().expect("Could not convert to str")), + Err(_) => js.push(script.to_str().expect("Could not convert to str")), + } + } + data.insert("additional_js".to_owned(), json!(js)); + } + + if html_config.playground.editable && html_config.playground.copy_js { + data.insert("playground_js".to_owned(), json!(true)); + if html_config.playground.line_numbers { + data.insert("playground_line_numbers".to_owned(), json!(true)); + } + } + if html_config.playground.copyable { + data.insert("playground_copyable".to_owned(), json!(true)); + } + + data.insert("print_enable".to_owned(), json!(html_config.print.enable)); + data.insert("fold_enable".to_owned(), json!(html_config.fold.enable)); + data.insert("fold_level".to_owned(), json!(html_config.fold.level)); + + let search = html_config.search.clone(); + if cfg!(feature = "search") { + let search = search.unwrap_or_default(); + data.insert("search_enabled".to_owned(), json!(search.enable)); + data.insert( + "search_js".to_owned(), + json!(search.enable && search.copy_js), + ); + } else if search.is_some() { + warn!("mdBook compiled without search support, ignoring `output.html.search` table"); + warn!( + "please reinstall with `cargo install mdbook --force --features search`to use the \ + search feature" + ) + } + + if let Some(ref git_repository_url) = html_config.git_repository_url { + data.insert("git_repository_url".to_owned(), json!(git_repository_url)); + } + + let git_repository_icon = match html_config.git_repository_icon { + Some(ref git_repository_icon) => git_repository_icon, + None => "fa-github", + }; + data.insert("git_repository_icon".to_owned(), json!(git_repository_icon)); + + let mut chapters = vec![]; + + for item in book.iter() { + // Create the data to inject in the template + let mut chapter = BTreeMap::new(); + + match *item { + BookItem::PartTitle(ref title) => { + chapter.insert("part".to_owned(), json!(title)); + } + BookItem::Chapter(ref ch) => { + if let Some(ref section) = ch.number { + chapter.insert("section".to_owned(), json!(section.to_string())); + } + + chapter.insert( + "has_sub_items".to_owned(), + json!((!ch.sub_items.is_empty()).to_string()), + ); + + chapter.insert("name".to_owned(), json!(ch.name)); + if let Some(ref path) = ch.path { + let p = path + .to_str() + .with_context(|| "Could not convert path to str")?; + chapter.insert("path".to_owned(), json!(p)); + } + } + BookItem::Separator => { + chapter.insert("spacer".to_owned(), json!("_spacer_")); + } + } + + chapters.push(chapter); + } + + data.insert("chapters".to_owned(), json!(chapters)); + + debug!("[*]: JSON constructed"); + Ok(data) +} + +/// Goes through the rendered HTML, making sure all header tags have +/// an anchor respectively so people can link to sections directly. +fn build_header_links(html: &str) -> String { + lazy_static! { + static ref BUILD_HEADER_LINKS: Regex = Regex::new(r"(.*?)").unwrap(); + } + + let mut id_counter = HashMap::new(); + + BUILD_HEADER_LINKS + .replace_all(html, |caps: &Captures<'_>| { + let level = caps[1] + .parse() + .expect("Regex should ensure we only ever get numbers here"); + + insert_link_into_header(level, &caps[2], &mut id_counter) + }) + .into_owned() +} + +/// Insert a sinle link into a header, making sure each link gets its own +/// unique ID by appending an auto-incremented number (if necessary). +fn insert_link_into_header( + level: usize, + content: &str, + id_counter: &mut HashMap, +) -> String { + let id = utils::unique_id_from_content(content, id_counter); + + format!( + r##"{text}"##, + level = level, + id = id, + text = content + ) +} + +// The rust book uses annotations for rustdoc to test code snippets, +// like the following: +// ```rust,should_panic +// fn main() { +// // Code here +// } +// ``` +// This function replaces all commas by spaces in the code block classes +fn fix_code_blocks(html: &str) -> String { + lazy_static! { + static ref FIX_CODE_BLOCKS: Regex = + Regex::new(r##"]+)class="([^"]+)"([^>]*)>"##).unwrap(); + } + + FIX_CODE_BLOCKS + .replace_all(html, |caps: &Captures<'_>| { + let before = &caps[1]; + let classes = &caps[2].replace(',', " "); + let after = &caps[3]; + + format!( + r#""#, + before = before, + classes = classes, + after = after + ) + }) + .into_owned() +} + +fn add_playground_pre( + html: &str, + playground_config: &Playground, + edition: Option, +) -> String { + lazy_static! { + static ref ADD_PLAYGROUND_PRE: Regex = + Regex::new(r##"((?s)]?class="([^"]+)".*?>(.*?))"##).unwrap(); + } + ADD_PLAYGROUND_PRE + .replace_all(html, |caps: &Captures<'_>| { + let text = &caps[1]; + let classes = &caps[2]; + let code = &caps[3]; + + if classes.contains("language-rust") { + if (!classes.contains("ignore") + && !classes.contains("noplayground") + && !classes.contains("noplaypen") + && playground_config.runnable) + || classes.contains("mdbook-runnable") + { + let contains_e2015 = classes.contains("edition2015"); + let contains_e2018 = classes.contains("edition2018"); + let contains_e2021 = classes.contains("edition2021"); + let edition_class = if contains_e2015 || contains_e2018 || contains_e2021 { + // the user forced edition, we should not overwrite it + "" + } else { + match edition { + Some(RustEdition::E2015) => " edition2015", + Some(RustEdition::E2018) => " edition2018", + Some(RustEdition::E2021) => " edition2021", + None => "", + } + }; + + // wrap the contents in an external pre block + format!( + "
{}
", + classes, + edition_class, + { + let content: Cow<'_, str> = if playground_config.editable + && classes.contains("editable") + || text.contains("fn main") + || text.contains("quick_main!") + { + code.into() + } else { + // we need to inject our own main + let (attrs, code) = partition_source(code); + + format!("# #![allow(unused)]\n{}#fn main() {{\n{}#}}", attrs, code) + .into() + }; + hide_lines(&content) + } + ) + } else { + format!("{}", classes, hide_lines(code)) + } + } else { + // not language-rust, so no-op + text.to_owned() + } + }) + .into_owned() +} + +fn hide_lines(content: &str) -> String { + lazy_static! { + static ref BORING_LINES_REGEX: Regex = Regex::new(r"^(\s*)#(.?)(.*)$").unwrap(); + } + + let mut result = String::with_capacity(content.len()); + for line in content.lines() { + if let Some(caps) = BORING_LINES_REGEX.captures(line) { + if &caps[2] == "#" { + result += &caps[1]; + result += &caps[2]; + result += &caps[3]; + result += "\n"; + continue; + } else if &caps[2] != "!" && &caps[2] != "[" { + result += ""; + result += &caps[1]; + if &caps[2] != " " { + result += &caps[2]; + } + result += &caps[3]; + result += "\n"; + result += ""; + continue; + } + } + result += line; + result += "\n"; + } + result +} + +fn partition_source(s: &str) -> (String, String) { + let mut after_header = false; + let mut before = String::new(); + let mut after = String::new(); + + for line in s.lines() { + let trimline = line.trim(); + let header = trimline.chars().all(char::is_whitespace) || trimline.starts_with("#!["); + if !header || after_header { + after_header = true; + after.push_str(line); + after.push('\n'); + } else { + before.push_str(line); + before.push('\n'); + } + } + + (before, after) +} + +struct RenderItemContext<'a> { + handlebars: &'a Handlebars<'a>, + destination: PathBuf, + data: serde_json::Map, + is_index: bool, + book_config: BookConfig, + html_config: HtmlConfig, + edition: Option, + chapter_titles: &'a HashMap, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn original_build_header_links() { + let inputs = vec![ + ( + "blah blah

Foo

", + r##"blah blah

Foo

"##, + ), + ( + "

Foo

", + r##"

Foo

"##, + ), + ( + "

Foo^bar

", + r##"

Foo^bar

"##, + ), + ( + "

", + r##"

"##, + ), + ( + "

", + r##"

"##, + ), + ( + "

Foo

Foo

", + r##"

Foo

Foo

"##, + ), + ]; + + for (src, should_be) in inputs { + let got = build_header_links(src); + assert_eq!(got, should_be); + } + } + + #[test] + fn add_playground() { + let inputs = [ + ("x()", + "
#![allow(unused)]\nfn main() {\nx()\n}\n
"), + ("fn main() {}", + "
fn main() {}\n
"), + ("let s = \"foo\n # bar\n\";", + "
let s = \"foo\n bar\n\";\n
"), + ("let s = \"foo\n ## bar\n\";", + "
let s = \"foo\n # bar\n\";\n
"), + ("let s = \"foo\n # bar\n#\n\";", + "
let s = \"foo\n bar\n\n\";\n
"), + ("let s = \"foo\n # bar\n\";", + "let s = \"foo\n bar\n\";\n"), + ("#![no_std]\nlet s = \"foo\";\n #[some_attr]", + "
#![no_std]\nlet s = \"foo\";\n #[some_attr]\n
"), + ]; + for (src, should_be) in &inputs { + let got = add_playground_pre( + src, + &Playground { + editable: true, + ..Playground::default() + }, + None, + ); + assert_eq!(&*got, *should_be); + } + } + #[test] + fn add_playground_edition2015() { + let inputs = [ + ("x()", + "
#![allow(unused)]\nfn main() {\nx()\n}\n
"), + ("fn main() {}", + "
fn main() {}\n
"), + ("fn main() {}", + "
fn main() {}\n
"), + ("fn main() {}", + "
fn main() {}\n
"), + ]; + for (src, should_be) in &inputs { + let got = add_playground_pre( + src, + &Playground { + editable: true, + ..Playground::default() + }, + Some(RustEdition::E2015), + ); + assert_eq!(&*got, *should_be); + } + } + #[test] + fn add_playground_edition2018() { + let inputs = [ + ("x()", + "
#![allow(unused)]\nfn main() {\nx()\n}\n
"), + ("fn main() {}", + "
fn main() {}\n
"), + ("fn main() {}", + "
fn main() {}\n
"), + ("fn main() {}", + "
fn main() {}\n
"), + ]; + for (src, should_be) in &inputs { + let got = add_playground_pre( + src, + &Playground { + editable: true, + ..Playground::default() + }, + Some(RustEdition::E2018), + ); + assert_eq!(&*got, *should_be); + } + } + #[test] + fn add_playground_edition2021() { + let inputs = [ + ("x()", + "
#![allow(unused)]\nfn main() {\nx()\n}\n
"), + ("fn main() {}", + "
fn main() {}\n
"), + ("fn main() {}", + "
fn main() {}\n
"), + ("fn main() {}", + "
fn main() {}\n
"), + ]; + for (src, should_be) in &inputs { + let got = add_playground_pre( + src, + &Playground { + editable: true, + ..Playground::default() + }, + Some(RustEdition::E2021), + ); + assert_eq!(&*got, *should_be); + } + } +} diff --git a/vendor/mdbook/src/renderer/html_handlebars/helpers/mod.rs b/vendor/mdbook/src/renderer/html_handlebars/helpers/mod.rs new file mode 100644 index 000000000..52be6d204 --- /dev/null +++ b/vendor/mdbook/src/renderer/html_handlebars/helpers/mod.rs @@ -0,0 +1,3 @@ +pub mod navigation; +pub mod theme; +pub mod toc; diff --git a/vendor/mdbook/src/renderer/html_handlebars/helpers/navigation.rs b/vendor/mdbook/src/renderer/html_handlebars/helpers/navigation.rs new file mode 100644 index 000000000..65929bbfc --- /dev/null +++ b/vendor/mdbook/src/renderer/html_handlebars/helpers/navigation.rs @@ -0,0 +1,290 @@ +use std::collections::BTreeMap; +use std::path::Path; + +use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError, Renderable}; + +use crate::utils; + +type StringMap = BTreeMap; + +/// Target for `find_chapter`. +enum Target { + Previous, + Next, +} + +impl Target { + /// Returns target if found. + fn find( + &self, + base_path: &str, + current_path: &str, + current_item: &StringMap, + previous_item: &StringMap, + ) -> Result, RenderError> { + match *self { + Target::Next => { + let previous_path = previous_item + .get("path") + .ok_or_else(|| RenderError::new("No path found for chapter in JSON data"))?; + + if previous_path == base_path { + return Ok(Some(current_item.clone())); + } + } + + Target::Previous => { + if current_path == base_path { + return Ok(Some(previous_item.clone())); + } + } + } + + Ok(None) + } +} + +fn find_chapter( + ctx: &Context, + rc: &mut RenderContext<'_, '_>, + target: Target, +) -> Result, RenderError> { + debug!("Get data from context"); + + let chapters = rc.evaluate(ctx, "@root/chapters").and_then(|c| { + serde_json::value::from_value::>(c.as_json().clone()) + .map_err(|_| RenderError::new("Could not decode the JSON data")) + })?; + + let base_path = rc + .evaluate(ctx, "@root/path")? + .as_json() + .as_str() + .ok_or_else(|| RenderError::new("Type error for `path`, string expected"))? + .replace('\"', ""); + + if !rc.evaluate(ctx, "@root/is_index")?.is_missing() { + // Special case for index.md which may be a synthetic page. + // Target::find won't match because there is no page with the path + // "index.md" (unless there really is an index.md in SUMMARY.md). + match target { + Target::Previous => return Ok(None), + Target::Next => match chapters + .iter() + .filter(|chapter| { + // Skip things like "spacer" + chapter.contains_key("path") + }) + .nth(1) + { + Some(chapter) => return Ok(Some(chapter.clone())), + None => return Ok(None), + }, + } + } + + let mut previous: Option = None; + + debug!("Search for chapter"); + + for item in chapters { + match item.get("path") { + Some(path) if !path.is_empty() => { + if let Some(previous) = previous { + if let Some(item) = target.find(&base_path, path, &item, &previous)? { + return Ok(Some(item)); + } + } + + previous = Some(item.clone()); + } + _ => continue, + } + } + + Ok(None) +} + +fn render( + _h: &Helper<'_, '_>, + r: &Handlebars<'_>, + ctx: &Context, + rc: &mut RenderContext<'_, '_>, + out: &mut dyn Output, + chapter: &StringMap, +) -> Result<(), RenderError> { + trace!("Creating BTreeMap to inject in context"); + + let mut context = BTreeMap::new(); + let base_path = rc + .evaluate(ctx, "@root/path")? + .as_json() + .as_str() + .ok_or_else(|| RenderError::new("Type error for `path`, string expected"))? + .replace('\"', ""); + + context.insert( + "path_to_root".to_owned(), + json!(utils::fs::path_to_root(&base_path)), + ); + + chapter + .get("name") + .ok_or_else(|| RenderError::new("No title found for chapter in JSON data")) + .map(|name| context.insert("title".to_owned(), json!(name)))?; + + chapter + .get("path") + .ok_or_else(|| RenderError::new("No path found for chapter in JSON data")) + .and_then(|p| { + Path::new(p) + .with_extension("html") + .to_str() + .ok_or_else(|| RenderError::new("Link could not be converted to str")) + .map(|p| context.insert("link".to_owned(), json!(p.replace('\\', "/")))) + })?; + + trace!("Render template"); + + _h.template() + .ok_or_else(|| RenderError::new("Error with the handlebars template")) + .and_then(|t| { + let local_ctx = Context::wraps(&context)?; + let mut local_rc = rc.clone(); + t.render(r, &local_ctx, &mut local_rc, out) + })?; + + Ok(()) +} + +pub fn previous( + _h: &Helper<'_, '_>, + r: &Handlebars<'_>, + ctx: &Context, + rc: &mut RenderContext<'_, '_>, + out: &mut dyn Output, +) -> Result<(), RenderError> { + trace!("previous (handlebars helper)"); + + if let Some(previous) = find_chapter(ctx, rc, Target::Previous)? { + render(_h, r, ctx, rc, out, &previous)?; + } + + Ok(()) +} + +pub fn next( + _h: &Helper<'_, '_>, + r: &Handlebars<'_>, + ctx: &Context, + rc: &mut RenderContext<'_, '_>, + out: &mut dyn Output, +) -> Result<(), RenderError> { + trace!("next (handlebars helper)"); + + if let Some(next) = find_chapter(ctx, rc, Target::Next)? { + render(_h, r, ctx, rc, out, &next)?; + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + static TEMPLATE: &str = + "{{#previous}}{{title}}: {{link}}{{/previous}}|{{#next}}{{title}}: {{link}}{{/next}}"; + + #[test] + fn test_next_previous() { + let data = json!({ + "name": "two", + "path": "two.path", + "chapters": [ + { + "name": "one", + "path": "one.path" + }, + { + "name": "two", + "path": "two.path", + }, + { + "name": "three", + "path": "three.path" + } + ] + }); + + let mut h = Handlebars::new(); + h.register_helper("previous", Box::new(previous)); + h.register_helper("next", Box::new(next)); + + assert_eq!( + h.render_template(TEMPLATE, &data).unwrap(), + "one: one.html|three: three.html" + ); + } + + #[test] + fn test_first() { + let data = json!({ + "name": "one", + "path": "one.path", + "chapters": [ + { + "name": "one", + "path": "one.path" + }, + { + "name": "two", + "path": "two.path", + }, + { + "name": "three", + "path": "three.path" + } + ] + }); + + let mut h = Handlebars::new(); + h.register_helper("previous", Box::new(previous)); + h.register_helper("next", Box::new(next)); + + assert_eq!( + h.render_template(TEMPLATE, &data).unwrap(), + "|two: two.html" + ); + } + #[test] + fn test_last() { + let data = json!({ + "name": "three", + "path": "three.path", + "chapters": [ + { + "name": "one", + "path": "one.path" + }, + { + "name": "two", + "path": "two.path", + }, + { + "name": "three", + "path": "three.path" + } + ] + }); + + let mut h = Handlebars::new(); + h.register_helper("previous", Box::new(previous)); + h.register_helper("next", Box::new(next)); + + assert_eq!( + h.render_template(TEMPLATE, &data).unwrap(), + "two: two.html|" + ); + } +} diff --git a/vendor/mdbook/src/renderer/html_handlebars/helpers/theme.rs b/vendor/mdbook/src/renderer/html_handlebars/helpers/theme.rs new file mode 100644 index 000000000..809ee1176 --- /dev/null +++ b/vendor/mdbook/src/renderer/html_handlebars/helpers/theme.rs @@ -0,0 +1,28 @@ +use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError}; + +pub fn theme_option( + h: &Helper<'_, '_>, + _r: &Handlebars<'_>, + ctx: &Context, + rc: &mut RenderContext<'_, '_>, + out: &mut dyn Output, +) -> Result<(), RenderError> { + trace!("theme_option (handlebars helper)"); + + let param = h.param(0).and_then(|v| v.value().as_str()).ok_or_else(|| { + RenderError::new("Param 0 with String type is required for theme_option helper.") + })?; + + let default_theme = rc.evaluate(ctx, "@root/default_theme")?; + let default_theme_name = default_theme + .as_json() + .as_str() + .ok_or_else(|| RenderError::new("Type error for `default_theme`, string expected"))?; + + out.write(param)?; + if param.to_lowercase() == default_theme_name.to_lowercase() { + out.write(" (default)")?; + } + + Ok(()) +} diff --git a/vendor/mdbook/src/renderer/html_handlebars/helpers/toc.rs b/vendor/mdbook/src/renderer/html_handlebars/helpers/toc.rs new file mode 100644 index 000000000..0884d30ad --- /dev/null +++ b/vendor/mdbook/src/renderer/html_handlebars/helpers/toc.rs @@ -0,0 +1,203 @@ +use std::path::Path; +use std::{cmp::Ordering, collections::BTreeMap}; + +use crate::utils; +use crate::utils::bracket_escape; + +use handlebars::{Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError}; + +// Handlebars helper to construct TOC +#[derive(Clone, Copy)] +pub struct RenderToc { + pub no_section_label: bool, +} + +impl HelperDef for RenderToc { + fn call<'reg: 'rc, 'rc>( + &self, + _h: &Helper<'reg, 'rc>, + _r: &'reg Handlebars<'_>, + ctx: &'rc Context, + rc: &mut RenderContext<'reg, 'rc>, + out: &mut dyn Output, + ) -> Result<(), RenderError> { + // get value from context data + // rc.get_path() is current json parent path, you should always use it like this + // param is the key of value you want to display + let chapters = rc.evaluate(ctx, "@root/chapters").and_then(|c| { + serde_json::value::from_value::>>(c.as_json().clone()) + .map_err(|_| RenderError::new("Could not decode the JSON data")) + })?; + let current_path = rc + .evaluate(ctx, "@root/path")? + .as_json() + .as_str() + .ok_or_else(|| RenderError::new("Type error for `path`, string expected"))? + .replace('\"', ""); + + let current_section = rc + .evaluate(ctx, "@root/section")? + .as_json() + .as_str() + .map(str::to_owned) + .unwrap_or_default(); + + let fold_enable = rc + .evaluate(ctx, "@root/fold_enable")? + .as_json() + .as_bool() + .ok_or_else(|| RenderError::new("Type error for `fold_enable`, bool expected"))?; + + let fold_level = rc + .evaluate(ctx, "@root/fold_level")? + .as_json() + .as_u64() + .ok_or_else(|| RenderError::new("Type error for `fold_level`, u64 expected"))?; + + out.write("
    ")?; + + let mut current_level = 1; + // The "index" page, which has this attribute set, is supposed to alias the first chapter in + // the book, i.e. the first link. There seems to be no easy way to determine which chapter + // the "index" is aliasing from within the renderer, so this is used instead to force the + // first link to be active. See further below. + let mut is_first_chapter = ctx.data().get("is_index").is_some(); + + for item in chapters { + // Spacer + if item.get("spacer").is_some() { + out.write("
  1. ")?; + continue; + } + + let (section, level) = if let Some(s) = item.get("section") { + (s.as_str(), s.matches('.').count()) + } else { + ("", 1) + }; + + let is_expanded = + if !fold_enable || (!section.is_empty() && current_section.starts_with(section)) { + // Expand if folding is disabled, or if the section is an + // ancestor or the current section itself. + true + } else { + // Levels that are larger than this would be folded. + level - 1 < fold_level as usize + }; + + match level.cmp(¤t_level) { + Ordering::Greater => { + while level > current_level { + out.write("
  2. ")?; + out.write("
      ")?; + current_level += 1; + } + write_li_open_tag(out, is_expanded, false)?; + } + Ordering::Less => { + while level < current_level { + out.write("
    ")?; + out.write("
  3. ")?; + current_level -= 1; + } + write_li_open_tag(out, is_expanded, false)?; + } + Ordering::Equal => { + write_li_open_tag(out, is_expanded, item.get("section").is_none())?; + } + } + + // Part title + if let Some(title) = item.get("part") { + out.write("
  4. ")?; + out.write(&bracket_escape(title))?; + out.write("
  5. ")?; + continue; + } + + // Link + let path_exists = if let Some(path) = + item.get("path") + .and_then(|p| if p.is_empty() { None } else { Some(p) }) + { + out.write("")?; + true + } else { + out.write("")?; + } + + // Render expand/collapse toggle + if let Some(flag) = item.get("has_sub_items") { + let has_sub_items = flag.parse::().unwrap_or_default(); + if fold_enable && has_sub_items { + out.write("
    ")?; + } + } + out.write("")?; + } + while current_level > 1 { + out.write("
")?; + out.write("")?; + current_level -= 1; + } + + out.write("")?; + Ok(()) + } +} + +fn write_li_open_tag( + out: &mut dyn Output, + is_expanded: bool, + is_affix: bool, +) -> Result<(), std::io::Error> { + let mut li = String::from("
  • "); + out.write(&li) +} diff --git a/vendor/mdbook/src/renderer/html_handlebars/mod.rs b/vendor/mdbook/src/renderer/html_handlebars/mod.rs new file mode 100644 index 000000000..f1155ed75 --- /dev/null +++ b/vendor/mdbook/src/renderer/html_handlebars/mod.rs @@ -0,0 +1,9 @@ +#![allow(missing_docs)] // FIXME: Document this + +pub use self::hbs_renderer::HtmlHandlebars; + +mod hbs_renderer; +mod helpers; + +#[cfg(feature = "search")] +mod search; diff --git a/vendor/mdbook/src/renderer/html_handlebars/search.rs b/vendor/mdbook/src/renderer/html_handlebars/search.rs new file mode 100644 index 000000000..c3b944c9d --- /dev/null +++ b/vendor/mdbook/src/renderer/html_handlebars/search.rs @@ -0,0 +1,286 @@ +use std::borrow::Cow; +use std::collections::{HashMap, HashSet}; +use std::path::Path; + +use elasticlunr::{Index, IndexBuilder}; +use pulldown_cmark::*; + +use crate::book::{Book, BookItem}; +use crate::config::Search; +use crate::errors::*; +use crate::theme::searcher; +use crate::utils; + +use serde::Serialize; + +const MAX_WORD_LENGTH_TO_INDEX: usize = 80; + +/// Tokenizes in the same way as elasticlunr-rs (for English), but also drops long tokens. +fn tokenize(text: &str) -> Vec { + text.split(|c: char| c.is_whitespace() || c == '-') + .filter(|s| !s.is_empty()) + .map(|s| s.trim().to_lowercase()) + .filter(|s| s.len() <= MAX_WORD_LENGTH_TO_INDEX) + .collect() +} + +/// Creates all files required for search. +pub fn create_files(search_config: &Search, destination: &Path, book: &Book) -> Result<()> { + let mut index = IndexBuilder::new() + .add_field_with_tokenizer("title", Box::new(&tokenize)) + .add_field_with_tokenizer("body", Box::new(&tokenize)) + .add_field_with_tokenizer("breadcrumbs", Box::new(&tokenize)) + .build(); + + let mut doc_urls = Vec::with_capacity(book.sections.len()); + + for item in book.iter() { + render_item(&mut index, search_config, &mut doc_urls, item)?; + } + + let index = write_to_json(index, search_config, doc_urls)?; + debug!("Writing search index ✓"); + if index.len() > 10_000_000 { + warn!("searchindex.json is very large ({} bytes)", index.len()); + } + + if search_config.copy_js { + utils::fs::write_file(destination, "searchindex.json", index.as_bytes())?; + utils::fs::write_file( + destination, + "searchindex.js", + format!("Object.assign(window.search, {});", index).as_bytes(), + )?; + utils::fs::write_file(destination, "searcher.js", searcher::JS)?; + utils::fs::write_file(destination, "mark.min.js", searcher::MARK_JS)?; + utils::fs::write_file(destination, "elasticlunr.min.js", searcher::ELASTICLUNR_JS)?; + debug!("Copying search files ✓"); + } + + Ok(()) +} + +/// Uses the given arguments to construct a search document, then inserts it to the given index. +fn add_doc( + index: &mut Index, + doc_urls: &mut Vec, + anchor_base: &str, + section_id: &Option, + items: &[&str], +) { + let url = if let Some(ref id) = *section_id { + Cow::Owned(format!("{}#{}", anchor_base, id)) + } else { + Cow::Borrowed(anchor_base) + }; + let url = utils::collapse_whitespace(url.trim()); + let doc_ref = doc_urls.len().to_string(); + doc_urls.push(url.into()); + + let items = items.iter().map(|&x| utils::collapse_whitespace(x.trim())); + index.add_doc(&doc_ref, items); +} + +/// Renders markdown into flat unformatted text and adds it to the search index. +fn render_item( + index: &mut Index, + search_config: &Search, + doc_urls: &mut Vec, + item: &BookItem, +) -> Result<()> { + let chapter = match *item { + BookItem::Chapter(ref ch) if !ch.is_draft_chapter() => ch, + _ => return Ok(()), + }; + + let chapter_path = chapter + .path + .as_ref() + .expect("Checked that path exists above"); + let filepath = Path::new(&chapter_path).with_extension("html"); + let filepath = filepath + .to_str() + .with_context(|| "Could not convert HTML path to str")?; + let anchor_base = utils::fs::normalize_path(filepath); + + let mut p = utils::new_cmark_parser(&chapter.content, false).peekable(); + + let mut in_heading = false; + let max_section_depth = u32::from(search_config.heading_split_level); + let mut section_id = None; + let mut heading = String::new(); + let mut body = String::new(); + let mut breadcrumbs = chapter.parent_names.clone(); + let mut footnote_numbers = HashMap::new(); + + breadcrumbs.push(chapter.name.clone()); + + let mut id_counter = HashMap::new(); + while let Some(event) = p.next() { + match event { + Event::Start(Tag::Heading(i, ..)) if i as u32 <= max_section_depth => { + if !heading.is_empty() { + // Section finished, the next heading is following now + // Write the data to the index, and clear it for the next section + add_doc( + index, + doc_urls, + &anchor_base, + §ion_id, + &[&heading, &body, &breadcrumbs.join(" » ")], + ); + section_id = None; + heading.clear(); + body.clear(); + breadcrumbs.pop(); + } + + in_heading = true; + } + Event::End(Tag::Heading(i, ..)) if i as u32 <= max_section_depth => { + in_heading = false; + section_id = Some(utils::unique_id_from_content(&heading, &mut id_counter)); + breadcrumbs.push(heading.clone()); + } + Event::Start(Tag::FootnoteDefinition(name)) => { + let number = footnote_numbers.len() + 1; + footnote_numbers.entry(name).or_insert(number); + } + Event::Html(html) => { + let mut html_block = html.into_string(); + + // As of pulldown_cmark 0.6, html events are no longer contained + // in an HtmlBlock tag. We must collect consecutive Html events + // into a block ourselves. + while let Some(Event::Html(html)) = p.peek() { + html_block.push_str(html); + p.next(); + } + + body.push_str(&clean_html(&html_block)); + } + Event::Start(_) | Event::End(_) | Event::Rule | Event::SoftBreak | Event::HardBreak => { + // Insert spaces where HTML output would usually separate text + // to ensure words don't get merged together + if in_heading { + heading.push(' '); + } else { + body.push(' '); + } + } + Event::Text(text) | Event::Code(text) => { + if in_heading { + heading.push_str(&text); + } else { + body.push_str(&text); + } + } + Event::FootnoteReference(name) => { + let len = footnote_numbers.len() + 1; + let number = footnote_numbers.entry(name).or_insert(len); + body.push_str(&format!(" [{}] ", number)); + } + Event::TaskListMarker(_checked) => {} + } + } + + if !body.is_empty() || !heading.is_empty() { + if heading.is_empty() { + if let Some(chapter) = breadcrumbs.first() { + heading = chapter.clone(); + } + } + // Make sure the last section is added to the index + add_doc( + index, + doc_urls, + &anchor_base, + §ion_id, + &[&heading, &body, &breadcrumbs.join(" » ")], + ); + } + + Ok(()) +} + +fn write_to_json(index: Index, search_config: &Search, doc_urls: Vec) -> Result { + use elasticlunr::config::{SearchBool, SearchOptions, SearchOptionsField}; + use std::collections::BTreeMap; + + #[derive(Serialize)] + struct ResultsOptions { + limit_results: u32, + teaser_word_count: u32, + } + + #[derive(Serialize)] + struct SearchindexJson { + /// The options used for displaying search results + results_options: ResultsOptions, + /// The searchoptions for elasticlunr.js + search_options: SearchOptions, + /// Used to lookup a document's URL from an integer document ref. + doc_urls: Vec, + /// The index for elasticlunr.js + index: elasticlunr::Index, + } + + let mut fields = BTreeMap::new(); + let mut opt = SearchOptionsField::default(); + let mut insert_boost = |key: &str, boost| { + opt.boost = Some(boost); + fields.insert(key.into(), opt); + }; + insert_boost("title", search_config.boost_title); + insert_boost("body", search_config.boost_paragraph); + insert_boost("breadcrumbs", search_config.boost_hierarchy); + + let search_options = SearchOptions { + bool: if search_config.use_boolean_and { + SearchBool::And + } else { + SearchBool::Or + }, + expand: search_config.expand, + fields, + }; + + let results_options = ResultsOptions { + limit_results: search_config.limit_results, + teaser_word_count: search_config.teaser_word_count, + }; + + let json_contents = SearchindexJson { + results_options, + search_options, + doc_urls, + index, + }; + + // By converting to serde_json::Value as an intermediary, we use a + // BTreeMap internally and can force a stable ordering of map keys. + let json_contents = serde_json::to_value(&json_contents)?; + let json_contents = serde_json::to_string(&json_contents)?; + + Ok(json_contents) +} + +fn clean_html(html: &str) -> String { + lazy_static! { + static ref AMMONIA: ammonia::Builder<'static> = { + let mut clean_content = HashSet::new(); + clean_content.insert("script"); + clean_content.insert("style"); + let mut builder = ammonia::Builder::new(); + builder + .tags(HashSet::new()) + .tag_attributes(HashMap::new()) + .generic_attributes(HashSet::new()) + .link_rel(None) + .allowed_classes(HashMap::new()) + .clean_content_tags(clean_content); + builder + }; + } + AMMONIA.clean(html).to_string() +} diff --git a/vendor/mdbook/src/renderer/markdown_renderer.rs b/vendor/mdbook/src/renderer/markdown_renderer.rs new file mode 100644 index 000000000..bd5def1f4 --- /dev/null +++ b/vendor/mdbook/src/renderer/markdown_renderer.rs @@ -0,0 +1,52 @@ +use crate::book::BookItem; +use crate::errors::*; +use crate::renderer::{RenderContext, Renderer}; +use crate::utils; + +use std::fs; + +#[derive(Default)] +/// A renderer to output the Markdown after the preprocessors have run. Mostly useful +/// when debugging preprocessors. +pub struct MarkdownRenderer; + +impl MarkdownRenderer { + /// Create a new `MarkdownRenderer` instance. + pub fn new() -> Self { + MarkdownRenderer + } +} + +impl Renderer for MarkdownRenderer { + fn name(&self) -> &str { + "markdown" + } + + fn render(&self, ctx: &RenderContext) -> Result<()> { + let destination = &ctx.destination; + let book = &ctx.book; + + if destination.exists() { + utils::fs::remove_dir_content(destination) + .with_context(|| "Unable to remove stale Markdown output")?; + } + + trace!("markdown render"); + for item in book.iter() { + if let BookItem::Chapter(ref ch) = *item { + if !ch.is_draft_chapter() { + utils::fs::write_file( + &ctx.destination, + &ch.path.as_ref().expect("Checked path exists before"), + ch.content.as_bytes(), + )?; + } + } + } + + fs::create_dir_all(&destination) + .with_context(|| "Unexpected error when constructing destination path")?; + + Ok(()) + } +} diff --git a/vendor/mdbook/src/renderer/mod.rs b/vendor/mdbook/src/renderer/mod.rs new file mode 100644 index 000000000..15465fbce --- /dev/null +++ b/vendor/mdbook/src/renderer/mod.rs @@ -0,0 +1,265 @@ +//! `mdbook`'s low level rendering interface. +//! +//! # Note +//! +//! You usually don't need to work with this module directly. If you want to +//! implement your own backend, then check out the [For Developers] section of +//! the user guide. +//! +//! The definition for [RenderContext] may be useful though. +//! +//! [For Developers]: https://rust-lang.github.io/mdBook/for_developers/index.html +//! [RenderContext]: struct.RenderContext.html + +pub use self::html_handlebars::HtmlHandlebars; +pub use self::markdown_renderer::MarkdownRenderer; + +mod html_handlebars; +mod markdown_renderer; + +use shlex::Shlex; +use std::collections::HashMap; +use std::fs; +use std::io::{self, ErrorKind, Read}; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; + +use crate::book::Book; +use crate::config::Config; +use crate::errors::*; +use toml::Value; + +use serde::{Deserialize, Serialize}; + +/// An arbitrary `mdbook` backend. +/// +/// Although it's quite possible for you to import `mdbook` as a library and +/// provide your own renderer, there are two main renderer implementations that +/// 99% of users will ever use: +/// +/// - [`HtmlHandlebars`] - the built-in HTML renderer +/// - [`CmdRenderer`] - a generic renderer which shells out to a program to do the +/// actual rendering +pub trait Renderer { + /// The `Renderer`'s name. + fn name(&self) -> &str; + + /// Invoke the `Renderer`, passing in all the necessary information for + /// describing a book. + fn render(&self, ctx: &RenderContext) -> Result<()>; +} + +/// The context provided to all renderers. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct RenderContext { + /// Which version of `mdbook` did this come from (as written in `mdbook`'s + /// `Cargo.toml`). Useful if you know the renderer is only compatible with + /// certain versions of `mdbook`. + pub version: String, + /// The book's root directory. + pub root: PathBuf, + /// A loaded representation of the book itself. + pub book: Book, + /// The loaded configuration file. + pub config: Config, + /// Where the renderer *must* put any build artefacts generated. To allow + /// renderers to cache intermediate results, this directory is not + /// guaranteed to be empty or even exist. + pub destination: PathBuf, + #[serde(skip)] + pub(crate) chapter_titles: HashMap, + #[serde(skip)] + __non_exhaustive: (), +} + +impl RenderContext { + /// Create a new `RenderContext`. + pub fn new(root: P, book: Book, config: Config, destination: Q) -> RenderContext + where + P: Into, + Q: Into, + { + RenderContext { + book, + config, + version: crate::MDBOOK_VERSION.to_string(), + root: root.into(), + destination: destination.into(), + chapter_titles: HashMap::new(), + __non_exhaustive: (), + } + } + + /// Get the source directory's (absolute) path on disk. + pub fn source_dir(&self) -> PathBuf { + self.root.join(&self.config.book.src) + } + + /// Load a `RenderContext` from its JSON representation. + pub fn from_json(reader: R) -> Result { + serde_json::from_reader(reader).with_context(|| "Unable to deserialize the `RenderContext`") + } +} + +/// A generic renderer which will shell out to an arbitrary executable. +/// +/// # Rendering Protocol +/// +/// When the renderer's `render()` method is invoked, `CmdRenderer` will spawn +/// the `cmd` as a subprocess. The `RenderContext` is passed to the subprocess +/// as a JSON string (using `serde_json`). +/// +/// > **Note:** The command used doesn't necessarily need to be a single +/// > executable (i.e. `/path/to/renderer`). The `cmd` string lets you pass +/// > in command line arguments, so there's no reason why it couldn't be +/// > `python /path/to/renderer --from mdbook --to epub`. +/// +/// Anything the subprocess writes to `stdin` or `stdout` will be passed through +/// to the user. While this gives the renderer maximum flexibility to output +/// whatever it wants, to avoid spamming users it is recommended to avoid +/// unnecessary output. +/// +/// To help choose the appropriate output level, the `RUST_LOG` environment +/// variable will be passed through to the subprocess, if set. +/// +/// If the subprocess wishes to indicate that rendering failed, it should exit +/// with a non-zero return code. +#[derive(Debug, Clone, PartialEq)] +pub struct CmdRenderer { + name: String, + cmd: String, +} + +impl CmdRenderer { + /// Create a new `CmdRenderer` which will invoke the provided `cmd` string. + pub fn new(name: String, cmd: String) -> CmdRenderer { + CmdRenderer { name, cmd } + } + + fn compose_command(&self, root: &Path, destination: &Path) -> Result { + let mut words = Shlex::new(&self.cmd); + let exe = match words.next() { + Some(e) => PathBuf::from(e), + None => bail!("Command string was empty"), + }; + + let exe = if exe.components().count() == 1 { + // Search PATH for the executable. + exe + } else { + // Relative paths are preferred to be relative to the book root. + let abs_exe = root.join(&exe); + if abs_exe.exists() { + abs_exe + } else { + // Historically paths were relative to the destination, but + // this is not the preferred way. + let legacy_path = destination.join(&exe); + if legacy_path.exists() { + warn!( + "Renderer command `{}` uses a path relative to the \ + renderer output directory `{}`. This was previously \ + accepted, but has been deprecated. Relative executable \ + paths should be relative to the book root.", + exe.display(), + destination.display() + ); + legacy_path + } else { + // Let this bubble through to later be handled by + // handle_render_command_error. + abs_exe + } + } + }; + + let mut cmd = Command::new(exe); + + for arg in words { + cmd.arg(arg); + } + + Ok(cmd) + } +} + +impl CmdRenderer { + fn handle_render_command_error(&self, ctx: &RenderContext, error: io::Error) -> Result<()> { + if let ErrorKind::NotFound = error.kind() { + // Look for "output.{self.name}.optional". + // If it exists and is true, treat this as a warning. + // Otherwise, fail the build. + + let optional_key = format!("output.{}.optional", self.name); + + let is_optional = match ctx.config.get(&optional_key) { + Some(Value::Boolean(value)) => *value, + _ => false, + }; + + if is_optional { + warn!( + "The command `{}` for backend `{}` was not found, \ + but was marked as optional.", + self.cmd, self.name + ); + return Ok(()); + } else { + error!( + "The command `{0}` wasn't found, is the \"{1}\" backend installed? \ + If you want to ignore this error when the \"{1}\" backend is not installed, \ + set `optional = true` in the `[output.{1}]` section of the book.toml configuration file.", + self.cmd, self.name + ); + } + } + Err(error).with_context(|| "Unable to start the backend")? + } +} + +impl Renderer for CmdRenderer { + fn name(&self) -> &str { + &self.name + } + + fn render(&self, ctx: &RenderContext) -> Result<()> { + info!("Invoking the \"{}\" renderer", self.name); + + let _ = fs::create_dir_all(&ctx.destination); + + let mut child = match self + .compose_command(&ctx.root, &ctx.destination)? + .stdin(Stdio::piped()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .current_dir(&ctx.destination) + .spawn() + { + Ok(c) => c, + Err(e) => return self.handle_render_command_error(ctx, e), + }; + + let mut stdin = child.stdin.take().expect("Child has stdin"); + if let Err(e) = serde_json::to_writer(&mut stdin, &ctx) { + // Looks like the backend hung up before we could finish + // sending it the render context. Log the error and keep going + warn!("Error writing the RenderContext to the backend, {}", e); + } + + // explicitly close the `stdin` file handle + drop(stdin); + + let status = child + .wait() + .with_context(|| "Error waiting for the backend to complete")?; + + trace!("{} exited with output: {:?}", self.cmd, status); + + if !status.success() { + error!("Renderer exited with non-zero return code."); + bail!("The \"{}\" renderer failed", self.name); + } else { + Ok(()) + } + } +} -- cgit v1.2.3