3 use std::fs::{self, File};
4 use std::io::prelude::*;
5 use std::io::{self, BufReader};
6 use std::lazy::SyncLazy as Lazy;
7 use std::path::{Component, Path, PathBuf};
9 use itertools::Itertools;
10 use rustc_data_structures::flock;
11 use rustc_data_structures::fx::{FxHashMap, FxHashSet};
14 use super::{collect_paths_for_type, ensure_trailing_slash, Context, BASIC_KEYWORDS};
15 use crate::clean::Crate;
16 use crate::config::{EmitType, RenderOptions};
17 use crate::docfs::PathError;
18 use crate::error::Error;
19 use crate::html::{layout, static_files};
21 static FILES_UNVERSIONED: Lazy<FxHashMap<&str, &[u8]>> = Lazy::new(|| {
23 "FiraSans-Regular.woff2" => static_files::fira_sans::REGULAR2,
24 "FiraSans-Medium.woff2" => static_files::fira_sans::MEDIUM2,
25 "FiraSans-Regular.woff" => static_files::fira_sans::REGULAR,
26 "FiraSans-Medium.woff" => static_files::fira_sans::MEDIUM,
27 "FiraSans-LICENSE.txt" => static_files::fira_sans::LICENSE,
28 "SourceSerif4-Regular.ttf.woff2" => static_files::source_serif_4::REGULAR2,
29 "SourceSerif4-Bold.ttf.woff2" => static_files::source_serif_4::BOLD2,
30 "SourceSerif4-It.ttf.woff2" => static_files::source_serif_4::ITALIC2,
31 "SourceSerif4-Regular.ttf.woff" => static_files::source_serif_4::REGULAR,
32 "SourceSerif4-Bold.ttf.woff" => static_files::source_serif_4::BOLD,
33 "SourceSerif4-It.ttf.woff" => static_files::source_serif_4::ITALIC,
34 "SourceSerif4-LICENSE.md" => static_files::source_serif_4::LICENSE,
35 "SourceCodePro-Regular.ttf.woff2" => static_files::source_code_pro::REGULAR2,
36 "SourceCodePro-Semibold.ttf.woff2" => static_files::source_code_pro::SEMIBOLD2,
37 "SourceCodePro-It.ttf.woff2" => static_files::source_code_pro::ITALIC2,
38 "SourceCodePro-Regular.ttf.woff" => static_files::source_code_pro::REGULAR,
39 "SourceCodePro-Semibold.ttf.woff" => static_files::source_code_pro::SEMIBOLD,
40 "SourceCodePro-It.ttf.woff" => static_files::source_code_pro::ITALIC,
41 "SourceCodePro-LICENSE.txt" => static_files::source_code_pro::LICENSE,
42 "noto-sans-kr-regular.woff2" => static_files::noto_sans_kr::REGULAR2,
43 "noto-sans-kr-regular.woff" => static_files::noto_sans_kr::REGULAR,
44 "noto-sans-kr-LICENSE.txt" => static_files::noto_sans_kr::LICENSE,
45 "LICENSE-MIT.txt" => static_files::LICENSE_MIT,
46 "LICENSE-APACHE.txt" => static_files::LICENSE_APACHE,
47 "COPYRIGHT.txt" => static_files::COPYRIGHT,
51 enum SharedResource<'a> {
52 /// This file will never change, no matter what toolchain is used to build it.
54 /// It does not have a resource suffix.
55 Unversioned { name: &'static str },
56 /// This file may change depending on the toolchain.
58 /// It has a resource suffix.
59 ToolchainSpecific { basename: &'static str },
60 /// This file may change for any crate within a build, or based on the CLI arguments.
62 /// This differs from normal invocation-specific files because it has a resource suffix.
63 InvocationSpecific { basename: &'a str },
66 impl SharedResource<'_> {
67 fn extension(&self) -> Option<&OsStr> {
68 use SharedResource::*;
71 | ToolchainSpecific { basename: name }
72 | InvocationSpecific { basename: name } => Path::new(name).extension(),
76 fn path(&self, cx: &Context<'_>) -> PathBuf {
78 SharedResource::Unversioned { name } => cx.dst.join(name),
79 SharedResource::ToolchainSpecific { basename } => cx.suffix_path(basename),
80 SharedResource::InvocationSpecific { basename } => cx.suffix_path(basename),
84 fn should_emit(&self, emit: &[EmitType]) -> bool {
88 let kind = match self {
89 SharedResource::Unversioned { .. } => EmitType::Unversioned,
90 SharedResource::ToolchainSpecific { .. } => EmitType::Toolchain,
91 SharedResource::InvocationSpecific { .. } => EmitType::InvocationSpecific,
98 fn suffix_path(&self, filename: &str) -> PathBuf {
99 // We use splitn vs Path::extension here because we might get a filename
100 // like `style.min.css` and we want to process that into
101 // `style-suffix.min.css`. Path::extension would just return `css`
102 // which would result in `style.min-suffix.css` which isn't what we
104 let (base, ext) = filename.split_once('.').unwrap();
105 let filename = format!("{}{}.{}", base, self.shared.resource_suffix, ext);
106 self.dst.join(&filename)
111 resource: SharedResource<'_>,
112 contents: impl 'static + Send + AsRef<[u8]>,
114 ) -> Result<(), Error> {
115 if resource.should_emit(emit) {
116 self.shared.fs.write(resource.path(self), contents)
124 resource: SharedResource<'_>,
125 contents: impl 'static + Send + AsRef<str> + AsRef<[u8]>,
128 ) -> Result<(), Error> {
130 let contents = contents.as_ref();
131 let contents = if resource.extension() == Some(&OsStr::new("css")) {
132 minifier::css::minify(contents).map_err(|e| {
133 Error::new(format!("failed to minify CSS file: {}", e), resource.path(self))
136 minifier::js::minify(contents)
138 self.write_shared(resource, contents, emit)
140 self.write_shared(resource, contents, emit)
145 pub(super) fn write_shared(
148 search_index: String,
149 options: &RenderOptions,
150 ) -> Result<(), Error> {
151 // Write out the shared files. Note that these are shared among all rustdoc
152 // docs placed in the output directory, so this needs to be a synchronized
153 // operation with respect to all other rustdocs running around.
154 let lock_file = cx.dst.join(".lock");
155 let _lock = try_err!(flock::Lock::new(&lock_file, true, true, true), &lock_file);
157 // Minified resources are usually toolchain resources. If they're not, they should use `cx.write_minify` directly.
159 basename: &'static str,
160 contents: impl 'static + Send + AsRef<str> + AsRef<[u8]>,
162 options: &RenderOptions,
163 ) -> Result<(), Error> {
165 SharedResource::ToolchainSpecific { basename },
167 options.enable_minification,
172 // Toolchain resources should never be dynamic.
173 let write_toolchain = |p: &'static _, c: &'static _| {
174 cx.write_shared(SharedResource::ToolchainSpecific { basename: p }, c, &options.emit)
177 // Crate resources should always be dynamic.
178 let write_crate = |p: &_, make_content: &dyn Fn() -> Result<Vec<u8>, Error>| {
179 let content = make_content()?;
180 cx.write_shared(SharedResource::InvocationSpecific { basename: p }, content, &options.emit)
183 fn add_background_image_to_css(
189 css.push_str(&format!(
190 "{} {{ background-image: url({}); }}",
192 SharedResource::ToolchainSpecific { basename: file }
201 // Add all the static files. These may already exist, but we just
202 // overwrite them anyway to make sure that they're fresh and up-to-date.
203 let mut rustdoc_css = static_files::RUSTDOC_CSS.to_owned();
204 add_background_image_to_css(
207 "details.undocumented[open] > summary::before, \
208 details.rustdoc-toggle[open] > summary::before, \
209 details.rustdoc-toggle[open] > summary.hideme::before",
212 add_background_image_to_css(
215 "details.undocumented > summary::before, details.rustdoc-toggle > summary::before",
218 write_minify("rustdoc.css", rustdoc_css, cx, options)?;
220 // Add all the static files. These may already exist, but we just
221 // overwrite them anyway to make sure that they're fresh and up-to-date.
222 write_minify("settings.css", static_files::SETTINGS_CSS, cx, options)?;
223 write_minify("noscript.css", static_files::NOSCRIPT_CSS, cx, options)?;
225 // To avoid "light.css" to be overwritten, we'll first run over the received themes and only
226 // then we'll run over the "official" styles.
227 let mut themes: FxHashSet<String> = FxHashSet::default();
229 for entry in &cx.shared.style_files {
230 let theme = try_none!(try_none!(entry.path.file_stem(), &entry.path).to_str(), &entry.path);
232 try_none!(try_none!(entry.path.extension(), &entry.path).to_str(), &entry.path);
234 // Handle the official themes
236 "light" => write_minify("light.css", static_files::themes::LIGHT, cx, options)?,
237 "dark" => write_minify("dark.css", static_files::themes::DARK, cx, options)?,
238 "ayu" => write_minify("ayu.css", static_files::themes::AYU, cx, options)?,
240 // Handle added third-party themes
241 let filename = format!("{}.{}", theme, extension);
242 write_crate(&filename, &|| Ok(try_err!(fs::read(&entry.path), &entry.path)))?;
246 themes.insert(theme.to_owned());
249 if (*cx.shared).layout.logo.is_empty() {
250 write_toolchain("rust-logo.png", static_files::RUST_LOGO)?;
252 if (*cx.shared).layout.favicon.is_empty() {
253 write_toolchain("favicon.svg", static_files::RUST_FAVICON_SVG)?;
254 write_toolchain("favicon-16x16.png", static_files::RUST_FAVICON_PNG_16)?;
255 write_toolchain("favicon-32x32.png", static_files::RUST_FAVICON_PNG_32)?;
257 write_toolchain("brush.svg", static_files::BRUSH_SVG)?;
258 write_toolchain("wheel.svg", static_files::WHEEL_SVG)?;
259 write_toolchain("clipboard.svg", static_files::CLIPBOARD_SVG)?;
260 write_toolchain("down-arrow.svg", static_files::DOWN_ARROW_SVG)?;
261 write_toolchain("toggle-minus.svg", static_files::TOGGLE_MINUS_PNG)?;
262 write_toolchain("toggle-plus.svg", static_files::TOGGLE_PLUS_PNG)?;
264 let mut themes: Vec<&String> = themes.iter().collect();
267 // FIXME: this should probably not be a toolchain file since it depends on `--theme`.
268 // But it seems a shame to copy it over and over when it's almost always the same.
269 // Maybe we can change the representation to move this out of main.js?
272 static_files::MAIN_JS
274 "/* INSERT THEMES HERE */",
275 &format!(" = {}", serde_json::to_string(&themes).unwrap()),
278 "/* INSERT RUSTDOC_VERSION HERE */",
281 rustc_interface::util::version_str().unwrap_or("unknown version")
287 write_minify("search.js", static_files::SEARCH_JS, cx, options)?;
288 write_minify("settings.js", static_files::SETTINGS_JS, cx, options)?;
290 if cx.include_sources {
291 write_minify("source-script.js", static_files::sidebar::SOURCE_SCRIPT, cx, options)?;
298 "var resourcesSuffix = \"{}\";{}",
299 cx.shared.resource_suffix,
300 static_files::STORAGE_JS
307 if let Some(ref css) = cx.shared.layout.css_file_extension {
308 let buffer = try_err!(fs::read_to_string(css), css);
309 // This varies based on the invocation, so it can't go through the write_minify wrapper.
311 SharedResource::InvocationSpecific { basename: "theme.css" },
313 options.enable_minification,
317 write_minify("normalize.css", static_files::NORMALIZE_CSS, cx, options)?;
318 for (name, contents) in &*FILES_UNVERSIONED {
319 cx.write_shared(SharedResource::Unversioned { name }, contents, &options.emit)?;
322 fn collect(path: &Path, krate: &str, key: &str) -> io::Result<(Vec<String>, Vec<String>)> {
323 let mut ret = Vec::new();
324 let mut krates = Vec::new();
327 let prefix = format!(r#"{}["{}"]"#, key, krate);
328 for line in BufReader::new(File::open(path)?).lines() {
330 if !line.starts_with(key) {
333 if line.starts_with(&prefix) {
336 ret.push(line.to_string());
338 line[key.len() + 2..]
341 .map(|s| s.to_owned())
342 .unwrap_or_else(String::new),
349 fn collect_json(path: &Path, krate: &str) -> io::Result<(Vec<String>, Vec<String>)> {
350 let mut ret = Vec::new();
351 let mut krates = Vec::new();
354 let prefix = format!("\"{}\"", krate);
355 for line in BufReader::new(File::open(path)?).lines() {
357 if !line.starts_with('"') {
360 if line.starts_with(&prefix) {
363 if line.ends_with(",\\") {
364 ret.push(line[..line.len() - 2].to_string());
366 // Ends with "\\" (it's the case for the last added crate line)
367 ret.push(line[..line.len() - 1].to_string());
371 .find(|s| !s.is_empty())
372 .map(|s| s.to_owned())
373 .unwrap_or_else(String::new),
380 use std::ffi::OsString;
385 children: FxHashMap<OsString, Hierarchy>,
386 elems: FxHashSet<OsString>,
390 fn new(elem: OsString) -> Hierarchy {
391 Hierarchy { elem, children: FxHashMap::default(), elems: FxHashSet::default() }
394 fn to_json_string(&self) -> String {
395 let mut subs: Vec<&Hierarchy> = self.children.values().collect();
396 subs.sort_unstable_by(|a, b| a.elem.cmp(&b.elem));
400 .map(|s| format!("\"{}\"", s.to_str().expect("invalid osstring conversion")))
401 .collect::<Vec<_>>();
402 files.sort_unstable();
403 let subs = subs.iter().map(|s| s.to_json_string()).collect::<Vec<_>>().join(",");
405 if subs.is_empty() { String::new() } else { format!(",\"dirs\":[{}]", subs) };
406 let files = files.join(",");
408 if files.is_empty() { String::new() } else { format!(",\"files\":[{}]", files) };
410 "{{\"name\":\"{name}\"{dirs}{files}}}",
411 name = self.elem.to_str().expect("invalid osstring conversion"),
418 if cx.include_sources {
419 let mut hierarchy = Hierarchy::new(OsString::new());
424 .filter_map(|p| p.0.strip_prefix(&cx.shared.src_root).ok())
426 let mut h = &mut hierarchy;
427 let mut elems = source
429 .filter_map(|s| match s {
430 Component::Normal(s) => Some(s.to_owned()),
435 let cur_elem = elems.next().expect("empty file path");
436 if elems.peek().is_none() {
437 h.elems.insert(cur_elem);
440 let e = cur_elem.clone();
441 h = h.children.entry(cur_elem.clone()).or_insert_with(|| Hierarchy::new(e));
446 let dst = cx.dst.join(&format!("source-files{}.js", cx.shared.resource_suffix));
447 let make_sources = || {
448 let (mut all_sources, _krates) =
449 try_err!(collect(&dst, &krate.name.as_str(), "sourcesIndex"), &dst);
450 all_sources.push(format!(
451 "sourcesIndex[\"{}\"] = {};",
453 hierarchy.to_json_string()
457 "var N = null;var sourcesIndex = {{}};\n{}\ncreateSourceSidebar();\n",
458 all_sources.join("\n")
462 write_crate("source-files.js", &make_sources)?;
465 // Update the search index and crate list.
466 let dst = cx.dst.join(&format!("search-index{}.js", cx.shared.resource_suffix));
467 let (mut all_indexes, mut krates) = try_err!(collect_json(&dst, &krate.name.as_str()), &dst);
468 all_indexes.push(search_index);
469 krates.push(krate.name.to_string());
472 // Sort the indexes by crate so the file will be generated identically even
473 // with rustdoc running in parallel.
475 write_crate("search-index.js", &|| {
476 let mut v = String::from("var searchIndex = JSON.parse('{\\\n");
477 v.push_str(&all_indexes.join(",\\\n"));
478 v.push_str("\\\n}');\nif (window.initSearch) {window.initSearch(searchIndex)};");
482 write_crate("crates.js", &|| {
483 let krates = krates.iter().map(|k| format!("\"{}\"", k)).join(",");
484 Ok(format!("window.ALL_CRATES = [{}];", krates).into_bytes())
487 if options.enable_index_page {
488 if let Some(index_page) = options.index_page.clone() {
489 let mut md_opts = options.clone();
490 md_opts.output = cx.dst.clone();
491 md_opts.external_html = (*cx.shared).layout.external_html.clone();
493 crate::markdown::render(&index_page, md_opts, cx.shared.edition())
494 .map_err(|e| Error::new(e, &index_page))?;
496 let dst = cx.dst.join("index.html");
497 let page = layout::Page {
498 title: "Index of crates",
501 static_root_path: cx.shared.static_root_path.as_deref(),
502 description: "List of crates",
503 keywords: BASIC_KEYWORDS,
504 resource_suffix: &cx.shared.resource_suffix,
506 static_extra_scripts: &[],
509 let content = format!(
511 <span class=\"in-band\">List of all crates</span>\
512 </h1><ul class=\"crate mod\">{}</ul>",
517 "<li><a class=\"crate mod\" href=\"{}index.html\">{}</a></li>",
518 ensure_trailing_slash(s),
524 let v = layout::render(
525 &cx.shared.templates,
530 &cx.shared.style_files,
532 cx.shared.fs.write(dst, v)?;
536 // Update the list of all implementors for traits
537 let dst = cx.dst.join("implementors");
538 let cache = cx.cache();
539 for (&did, imps) in &cache.implementors {
540 // Private modules can leak through to this phase of rustdoc, which
541 // could contain implementations for otherwise private types. In some
542 // rare cases we could find an implementation for an item which wasn't
543 // indexed, so we just skip this step in that case.
545 // FIXME: this is a vague explanation for why this can't be a `get`, in
546 // theory it should be...
547 let &(ref remote_path, remote_item_type) = match cache.paths.get(&did) {
549 None => match cache.external_paths.get(&did) {
562 let implementors = imps
565 // If the trait and implementation are in the same crate, then
566 // there's no need to emit information about it (there's inlining
567 // going on). If they're in different crates then the crate defining
568 // the trait will be interested in our implementation.
570 // If the implementation is from another crate then that crate
572 if imp.impl_item.def_id.krate() == did.krate || !imp.impl_item.def_id.is_local() {
576 text: imp.inner_impl().print(false, cx).to_string(),
577 synthetic: imp.inner_impl().synthetic,
578 types: collect_paths_for_type(imp.inner_impl().for_.clone(), cache),
582 .collect::<Vec<_>>();
584 // Only create a js file if we have impls to add to it. If the trait is
585 // documented locally though we always create the file to avoid dead
587 if implementors.is_empty() && !cache.paths.contains_key(&did) {
591 let implementors = format!(
592 r#"implementors["{}"] = {};"#,
594 serde_json::to_string(&implementors).unwrap()
597 let mut mydst = dst.clone();
598 for part in &remote_path[..remote_path.len() - 1] {
601 cx.shared.ensure_dir(&mydst)?;
602 mydst.push(&format!("{}.{}.js", remote_item_type, remote_path[remote_path.len() - 1]));
604 let (mut all_implementors, _) =
605 try_err!(collect(&mydst, &krate.name.as_str(), "implementors"), &mydst);
606 all_implementors.push(implementors);
607 // Sort the implementors by crate so the file will be generated
608 // identically even with rustdoc running in parallel.
609 all_implementors.sort();
611 let mut v = String::from("(function() {var implementors = {};\n");
612 for implementor in &all_implementors {
613 writeln!(v, "{}", *implementor).unwrap();
616 "if (window.register_implementors) {\
617 window.register_implementors(implementors);\
619 window.pending_implementors = implementors;\
623 cx.shared.fs.write(mydst, v)?;