1 use crate::book
::{Book, BookItem}
;
2 use crate::config
::{Config, HtmlConfig, Playground, RustEdition}
;
4 use crate::renderer
::html_handlebars
::helpers
;
5 use crate::renderer
::{RenderContext, Renderer}
;
6 use crate::theme
::{self, playground_editor, Theme}
;
10 use std
::collections
::BTreeMap
;
11 use std
::collections
::HashMap
;
12 use std
::fs
::{self, File}
;
13 use std
::path
::{Path, PathBuf}
;
15 use crate::utils
::fs
::get_404_output_file
;
16 use handlebars
::Handlebars
;
17 use regex
::{Captures, Regex}
;
20 pub struct HtmlHandlebars
;
23 pub fn new() -> Self {
30 mut ctx
: RenderItemContext
<'_
>,
31 print_content
: &mut String
,
33 // FIXME: This should be made DRY-er and rely less on mutable state
35 let (ch
, path
) = match item
{
36 BookItem
::Chapter(ch
) if !ch
.is_draft_chapter() => (ch
, ch
.path
.as_ref().unwrap()),
40 let content
= ch
.content
.clone();
41 let content
= utils
::render_markdown(&content
, ctx
.html_config
.curly_quotes
);
43 let fixed_content
= utils
::render_markdown_with_path(
45 ctx
.html_config
.curly_quotes
,
48 print_content
.push_str(&fixed_content
);
50 // Update the context with data for this file
53 .with_context(|| "Could not convert path to str")?
;
54 let filepath
= Path
::new(&ctx_path
).with_extension("html");
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());
64 .and_then(serde_json
::Value
::as_str
)
67 let title
= match book_title
{
68 "" => ch
.name
.clone(),
69 _
=> ch
.name
.clone() + " - " + book_title
,
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
));
77 "path_to_root".to_owned(),
78 json
!(utils
::fs
::path_to_root(&path
)),
80 if let Some(ref section
) = ch
.number
{
82 .insert("section".to_owned(), json
!(section
.to_string()));
85 // Render the handlebars template with the data
86 debug
!("Render template");
87 let rendered
= ctx
.handlebars
.render("index", &ctx
.data
)?
;
89 let rendered
= self.post_process(rendered
, &ctx
.html_config
.playground
, ctx
.edition
);
92 debug
!("Creating {}", filepath
.display());
93 utils
::fs
::write_file(&ctx
.destination
, &filepath
, rendered
.as_bytes())?
;
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
)?
;
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())?
;
112 html_config
: &HtmlConfig
,
114 handlebars
: &mut Handlebars
<'_
>,
115 data
: &mut serde_json
::Map
<String
, serde_json
::Value
>,
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
))?
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
)
130 "# Document not found (404)\n\nThis URL is invalid, sorry. Please use the \
131 navigation bar or search to continue."
135 let html_content_404
= utils
::render_markdown(&content_404
, html_config
.curly_quotes
);
137 let mut data_404
= data
.clone();
138 let base_url
= if let Some(site_url
) = &html_config
.site_url
{
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."
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
)?
;
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 ✓");
162 #[cfg_attr(feature = "cargo-clippy", allow(clippy::let_and_return))]
166 playground_config
: &Playground
,
167 edition
: Option
<RustEdition
>,
169 let rendered
= build_header_links(&rendered
);
170 let rendered
= fix_code_blocks(&rendered
);
171 let rendered
= add_playground_pre(&rendered
, playground_config
, edition
);
176 fn copy_static_files(
180 html_config
: &HtmlConfig
,
182 use crate::utils
::fs
::write_file
;
187 b
"This file makes sure that Github Pages doesn't process mdBook's output.\n",
190 if let Some(cname
) = &html_config
.cname
{
191 write_file(destination
, "CNAME", format
!("{}\n", cname
).as_bytes())?
;
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
)?
;
202 if let Some(contents
) = &theme
.favicon_svg
{
203 write_file(destination
, "favicon.svg", &contents
)?
;
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
)?
;
212 "FontAwesome/css/font-awesome.css",
217 "FontAwesome/fonts/fontawesome-webfont.eot",
218 theme
::FONT_AWESOME_EOT
,
222 "FontAwesome/fonts/fontawesome-webfont.svg",
223 theme
::FONT_AWESOME_SVG
,
227 "FontAwesome/fonts/fontawesome-webfont.ttf",
228 theme
::FONT_AWESOME_TTF
,
232 "FontAwesome/fonts/fontawesome-webfont.woff",
233 theme
::FONT_AWESOME_WOFF
,
237 "FontAwesome/fonts/fontawesome-webfont.woff2",
238 theme
::FONT_AWESOME_WOFF2
,
242 "FontAwesome/fonts/FontAwesome.ttf",
243 theme
::FONT_AWESOME_TTF
,
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
)?
;
250 for (file_name
, contents
) in theme
::fonts
::OPEN_SANS
.iter() {
251 write_file(destination
, file_name
, contents
)?
;
255 theme
::fonts
::SOURCE_CODE_PRO
.0,
256 theme
::fonts
::SOURCE_CODE_PRO
.1,
260 let playground_config
= &html_config
.playground
;
262 // Ace is a very large dependency, so only load it when requested
263 if playground_config
.editable
&& playground_config
.copy_js
{
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
)?
;
271 playground_editor
::THEME_DAWN_JS
,
275 "theme-tomorrow_night.js",
276 playground_editor
::THEME_TOMORROW_NIGHT_JS
,
283 /// Update the context with data for this file
284 fn configure_print_version(
286 data
: &mut serde_json
::Map
<String
, serde_json
::Value
>,
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
));
296 "path_to_root".to_owned(),
297 json
!(utils
::fs
::path_to_root(Path
::new("print.md"))),
301 fn register_hbs_helpers(&self, handlebars
: &mut Handlebars
<'_
>, html_config
: &HtmlConfig
) {
302 handlebars
.register_helper(
304 Box
::new(helpers
::toc
::RenderToc
{
305 no_section_label
: html_config
.no_section_label
,
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
));
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(
321 let custom_files
= html
.additional_css
.iter().chain(html
.additional_js
.iter());
323 debug
!("Copying additional CSS and JS");
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()))?
;
334 input_location
.display(),
335 output_location
.display()
338 fs
::copy(&input_location
, &output_location
).with_context(|| {
340 "Unable to copy {} to {}",
341 input_location
.display(),
342 output_location
.display()
353 handlebars
: &Handlebars
<'_
>,
354 redirects
: &HashMap
<String
, String
>,
356 if redirects
.is_empty() {
360 log
::debug
!("Emitting redirects");
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
)?
;
377 handlebars
: &Handlebars
<'_
>,
381 if original
.exists() {
382 // sanity check to avoid accidentally overwriting a real file.
384 "Not redirecting \"{}\" to \"{}\" because it already exists. Are you sure it needs to be redirected?",
388 return Err(Error
::msg(msg
));
391 if let Some(parent
) = original
.parent() {
392 std
::fs
::create_dir_all(parent
)
393 .with_context(|| format
!("Unable to ensure \"{}\" exists", parent
.display()))?
;
399 let f
= File
::create(original
)?
;
401 .render_to_write("redirect", &ctx
, f
)
404 "Unable to create a redirect file at \"{}\"",
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"))
421 for entry
in fs
::read_dir(dir
)?
{
422 if entry_is_maybe_book_file(entry?
).unwrap_or(false) {
432 impl Renderer
for HtmlHandlebars
{
433 fn name(&self) -> &str {
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
);
444 if destination
.exists() {
445 utils
::fs
::remove_dir_content(destination
)
446 .with_context(|| "Unable to remove stale HTML output")?
;
450 let mut handlebars
= Handlebars
::new();
452 let theme_dir
= match html_config
.theme
{
453 Some(ref theme
) => theme
.to_path_buf(),
454 None
=> ctx
.root
.join("theme"),
457 if html_config
.theme
.is_none()
458 && maybe_wrong_theme_dir(&src_dir
.join("theme")).unwrap_or(false)
461 "Previous versions of mdBook erroneously accepted `./src/theme` as an automatic \
464 warn
!("Please move your theme files to `./theme` for them to continue being used");
467 let theme
= theme
::Theme
::new(theme_dir
);
469 debug
!("Register the index handlebars template");
470 handlebars
.register_template_string("index", String
::from_utf8(theme
.index
.clone())?
)?
;
472 debug
!("Register the head handlebars template");
473 handlebars
.register_partial("head", String
::from_utf8(theme
.head
.clone())?
)?
;
475 debug
!("Register the redirect handlebars template");
477 .register_template_string("redirect", String
::from_utf8(theme
.redirect
.clone())?
)?
;
479 debug
!("Register the header handlebars template");
480 handlebars
.register_partial("header", String
::from_utf8(theme
.header
.clone())?
)?
;
482 debug
!("Register handlebars helpers");
483 self.register_hbs_helpers(&mut handlebars
, &html_config
);
485 let mut data
= make_data(&ctx
.root
, &book
, &ctx
.config
, &html_config
, &theme
)?
;
488 let mut print_content
= String
::new();
490 fs
::create_dir_all(&destination
)
491 .with_context(|| "Unexpected error when constructing destination path")?
;
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(),
500 html_config
: html_config
.clone(),
501 edition
: ctx
.config
.rust
.edition
,
503 self.render_item(item
, ctx
, &mut print_content
)?
;
508 if html_config
.input_404
!= Some("".to_string()) {
509 self.render_404(ctx
, &html_config
, &src_dir
, &mut handlebars
, &mut data
)?
;
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
));
518 // Render the handlebars template with the data
519 debug
!("Render template");
520 let rendered
= handlebars
.render("index", &data
)?
;
523 self.post_process(rendered
, &html_config
.playground
, ctx
.config
.rust
.edition
);
525 utils
::fs
::write_file(&destination
, "print.html", rendered
.as_bytes())?
;
526 debug
!("Creating print.html ✓");
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")?
;
534 // Render search index
535 #[cfg(feature = "search")]
537 let search
= html_config
.search
.unwrap_or_default();
539 super::search
::create_files(&search
, &destination
, &book
)?
;
543 self.emit_redirects(&ctx
.destination
, &handlebars
, &html_config
.redirect
)
544 .context("Unable to emit redirects")?
;
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"])?
;
557 html_config
: &HtmlConfig
,
559 ) -> Result
<serde_json
::Map
<String
, serde_json
::Value
>> {
562 let mut data
= serde_json
::Map
::new();
564 "language".to_owned(),
565 json
!(config
.book
.language
.clone().unwrap_or_default()),
568 "book_title".to_owned(),
569 json
!(config
.book
.title
.clone().unwrap_or_default()),
572 "description".to_owned(),
573 json
!(config
.book
.description
.clone().unwrap_or_default()),
575 if theme
.favicon_png
.is_some() {
576 data
.insert("favicon_png".to_owned(), json
!("favicon.png"));
578 if theme
.favicon_svg
.is_some() {
579 data
.insert("favicon_svg".to_owned(), json
!("favicon.svg"));
581 if let Some(ref livereload
) = html_config
.livereload_url
{
582 data
.insert("livereload".to_owned(), json
!(livereload
));
585 let default_theme
= match html_config
.default_theme
{
586 Some(ref theme
) => theme
.to_lowercase(),
587 None
=> "light".to_string(),
589 data
.insert("default_theme".to_owned(), json
!(default_theme
));
591 let preferred_dark_theme
= match html_config
.preferred_dark_theme
{
592 Some(ref theme
) => theme
.to_lowercase(),
593 None
=> "navy".to_string(),
596 "preferred_dark_theme".to_owned(),
597 json
!(preferred_dark_theme
),
600 // Add google analytics tag
601 if let Some(ref ga
) = html_config
.google_analytics
{
602 data
.insert("google_analytics".to_owned(), json
!(ga
));
605 if html_config
.mathjax_support
{
606 data
.insert("mathjax_support".to_owned(), json
!(true));
609 if html_config
.copy_fonts
{
610 data
.insert("copy_fonts".to_owned(), json
!(true));
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")),
622 data
.insert("additional_css".to_owned(), json
!(css
));
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")),
634 data
.insert("additional_js".to_owned(), json
!(js
));
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));
643 if html_config
.playground
.copyable
{
644 data
.insert("playground_copyable".to_owned(), json
!(true));
647 data
.insert("fold_enable".to_owned(), json
!((html_config
.fold
.enable
)));
648 data
.insert("fold_level".to_owned(), json
!((html_config
.fold
.level
)));
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
));
655 "search_js".to_owned(),
656 json
!(search
.enable
&& search
.copy_js
),
658 } else if search
.is_some() {
659 warn
!("mdBook compiled without search support, ignoring `output.html.search` table");
661 "please reinstall with `cargo install mdbook --force --features search`to use the \
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
));
670 let git_repository_icon
= match html_config
.git_repository_icon
{
671 Some(ref git_repository_icon
) => git_repository_icon
,
674 data
.insert("git_repository_icon".to_owned(), json
!(git_repository_icon
));
676 let mut chapters
= vec
![];
678 for item
in book
.iter() {
679 // Create the data to inject in the template
680 let mut chapter
= BTreeMap
::new();
683 BookItem
::PartTitle(ref title
) => {
684 chapter
.insert("part".to_owned(), json
!(title
));
686 BookItem
::Chapter(ref ch
) => {
687 if let Some(ref section
) = ch
.number
{
688 chapter
.insert("section".to_owned(), json
!(section
.to_string()));
692 "has_sub_items".to_owned(),
693 json
!((!ch
.sub_items
.is_empty()).to_string()),
696 chapter
.insert("name".to_owned(), json
!(ch
.name
));
697 if let Some(ref path
) = ch
.path
{
700 .with_context(|| "Could not convert path to str")?
;
701 chapter
.insert("path".to_owned(), json
!(p
));
704 BookItem
::Separator
=> {
705 chapter
.insert("spacer".to_owned(), json
!("_spacer_"));
709 chapters
.push(chapter
);
712 data
.insert("chapters".to_owned(), json
!(chapters
));
714 debug
!("[*]: JSON constructed");
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();
725 .replace_all(html
, |caps
: &Captures
<'_
>| {
728 .expect("Regex should ensure we only ever get numbers here");
730 insert_link_into_header(level
, &caps
[2], &mut id_counter
)
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(
740 id_counter
: &mut HashMap
<String
, usize>,
742 let raw_id
= utils
::id_from_content(content
);
744 let id_count
= id_counter
.entry(raw_id
.clone()).or_insert(0);
746 let id
= match *id_count
{
748 other
=> format
!("{}-{}", raw_id
, other
),
754 r
##"<h{level}><a class="header" href="#{id}" id="{id}">{text}</a></h{level}>"##,
761 // The rust book uses annotations for rustdoc to test code snippets,
762 // like the following:
763 // ```rust,should_panic
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();
772 .replace_all(html, |caps: &Captures<'_>| {
773 let before = &caps[1];
774 let classes = &caps[2].replace(",", " ");
775 let after = &caps[3];
778 r#"<code{before}class="{classes}"{after}>"#,
787 fn add_playground_pre(
789 playground_config: &Playground,
790 edition: Option<RustEdition>,
792 let regex = Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap();
794 .replace_all(html
, |caps
: &Captures
<'_
>| {
796 let classes
= &caps
[2];
799 if classes
.contains("language-rust") {
800 if (!classes
.contains("ignore")
801 && !classes
.contains("noplayground")
802 && !classes
.contains("noplaypen"))
803 || classes
.contains("mdbook-runnable")
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
812 Some(RustEdition
::E2015
) => " edition2015",
813 Some(RustEdition
::E2018
) => " edition2018",
818 // wrap the contents in an external pre block
820 "<pre class=\"playground\"><code class=\"{}{}\">{}</code></pre>",
824 let content
: Cow
<'_
, str> = if playground_config
.editable
825 && classes
.contains("editable")
826 || text
.contains("fn main")
827 || text
.contains("quick_main!")
831 // we need to inject our own main
832 let (attrs
, code
) = partition_source(code
);
835 "\n# #![allow(unused)]\n{}#fn main() {{\n{}#}}",
844 format
!("<code class=\"{}\">{}</code>", classes
, hide_lines(code
))
847 // not language-rust, so no-op
855 static ref BORING_LINES_REGEX
: Regex
= Regex
::new(r
"^(\s*)#(.?)(.*)$").unwrap();
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
) {
868 } else if &caps
[2] != "!" && &caps
[2] != "[" {
869 result
+= "<span class=\"boring\">";
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();
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
{
896 after
.push_str(line
);
897 after
.push_str("\n");
899 before
.push_str(line
);
900 before
.push_str("\n");
907 struct RenderItemContext
<'a
> {
908 handlebars
: &'a Handlebars
<'a
>,
909 destination
: PathBuf
,
910 data
: serde_json
::Map
<String
, serde_json
::Value
>,
912 html_config
: HtmlConfig
,
913 edition
: Option
<RustEdition
>,
921 fn original_build_header_links() {
924 "blah blah <h1>Foo</h1>",
925 r
##"blah blah <h1><a class="header" href="#foo" id="foo">Foo</a></h1>"##,
929 r
##"<h1><a class="header" href="#foo" id="foo">Foo</a></h1>"##,
933 r
##"<h3><a class="header" href="#foobar" id="foobar">Foo^bar</a></h3>"##,
937 r
##"<h4><a class="header" href="#" id=""></a></h4>"##,
940 "<h4><em>Hï</em></h4>",
941 r
##"<h4><a class="header" href="#hï" id="hï"><em>Hï</em></a></h4>"##,
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>"##,
949 for (src
, should_be
) in inputs
{
950 let got
= build_header_links(&src
);
951 assert_eq
!(got
, should_be
);
956 fn add_playground() {
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>"),
973 for (src
, should_be
) in &inputs
{
974 let got
= add_playground_pre(
978 ..Playground
::default()
982 assert_eq
!(&*got
, *should_be
);
986 fn add_playground_edition2015() {
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>"),
997 for (src
, should_be
) in &inputs
{
998 let got
= add_playground_pre(
1002 ..Playground
::default()
1004 Some(RustEdition
::E2015
),
1006 assert_eq
!(&*got
, *should_be
);
1010 fn add_playground_edition2018() {
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>"),
1021 for (src
, should_be
) in &inputs
{
1022 let got
= add_playground_pre(
1026 ..Playground
::default()
1028 Some(RustEdition
::E2018
),
1030 assert_eq
!(&*got
, *should_be
);