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