2 use std::fs::{self, File};
3 use std::io::prelude::*;
4 use std::io::{self, BufReader};
5 use std::path::{Component, Path, PathBuf};
7 use std::sync::LazyLock as Lazy;
9 use itertools::Itertools;
10 use rustc_data_structures::flock;
11 use rustc_data_structures::fx::{FxHashMap, FxHashSet};
12 use serde::ser::SerializeSeq;
13 use serde::{Serialize, Serializer};
15 use super::{collect_paths_for_type, ensure_trailing_slash, Context, BASIC_KEYWORDS};
16 use crate::clean::Crate;
17 use crate::config::{EmitType, RenderOptions};
18 use crate::docfs::PathError;
19 use crate::error::Error;
20 use crate::html::{layout, static_files};
21 use crate::{try_err, try_none};
23 static FILES_UNVERSIONED: Lazy<FxHashMap<&str, &[u8]>> = Lazy::new(|| {
25 "FiraSans-Regular.woff2" => static_files::fira_sans::REGULAR,
26 "FiraSans-Medium.woff2" => static_files::fira_sans::MEDIUM,
27 "FiraSans-LICENSE.txt" => static_files::fira_sans::LICENSE,
28 "SourceSerif4-Regular.ttf.woff2" => static_files::source_serif_4::REGULAR,
29 "SourceSerif4-Bold.ttf.woff2" => static_files::source_serif_4::BOLD,
30 "SourceSerif4-It.ttf.woff2" => static_files::source_serif_4::ITALIC,
31 "SourceSerif4-LICENSE.md" => static_files::source_serif_4::LICENSE,
32 "SourceCodePro-Regular.ttf.woff2" => static_files::source_code_pro::REGULAR,
33 "SourceCodePro-Semibold.ttf.woff2" => static_files::source_code_pro::SEMIBOLD,
34 "SourceCodePro-It.ttf.woff2" => static_files::source_code_pro::ITALIC,
35 "SourceCodePro-LICENSE.txt" => static_files::source_code_pro::LICENSE,
36 "NanumBarunGothic.ttf.woff2" => static_files::nanum_barun_gothic::REGULAR,
37 "NanumBarunGothic-LICENSE.txt" => static_files::nanum_barun_gothic::LICENSE,
38 "LICENSE-MIT.txt" => static_files::LICENSE_MIT,
39 "LICENSE-APACHE.txt" => static_files::LICENSE_APACHE,
40 "COPYRIGHT.txt" => static_files::COPYRIGHT,
44 enum SharedResource<'a> {
45 /// This file will never change, no matter what toolchain is used to build it.
47 /// It does not have a resource suffix.
48 Unversioned { name: &'static str },
49 /// This file may change depending on the toolchain.
51 /// It has a resource suffix.
52 ToolchainSpecific { basename: &'static str },
53 /// This file may change for any crate within a build, or based on the CLI arguments.
55 /// This differs from normal invocation-specific files because it has a resource suffix.
56 InvocationSpecific { basename: &'a str },
59 impl SharedResource<'_> {
60 fn extension(&self) -> Option<&OsStr> {
61 use SharedResource::*;
64 | ToolchainSpecific { basename: name }
65 | InvocationSpecific { basename: name } => Path::new(name).extension(),
69 fn path(&self, cx: &Context<'_>) -> PathBuf {
71 SharedResource::Unversioned { name } => cx.dst.join(name),
72 SharedResource::ToolchainSpecific { basename } => cx.suffix_path(basename),
73 SharedResource::InvocationSpecific { basename } => cx.suffix_path(basename),
77 fn should_emit(&self, emit: &[EmitType]) -> bool {
81 let kind = match self {
82 SharedResource::Unversioned { .. } => EmitType::Unversioned,
83 SharedResource::ToolchainSpecific { .. } => EmitType::Toolchain,
84 SharedResource::InvocationSpecific { .. } => EmitType::InvocationSpecific,
91 fn suffix_path(&self, filename: &str) -> PathBuf {
92 // We use splitn vs Path::extension here because we might get a filename
93 // like `style.min.css` and we want to process that into
94 // `style-suffix.min.css`. Path::extension would just return `css`
95 // which would result in `style.min-suffix.css` which isn't what we
97 let (base, ext) = filename.split_once('.').unwrap();
98 let filename = format!("{}{}.{}", base, self.shared.resource_suffix, ext);
99 self.dst.join(&filename)
104 resource: SharedResource<'_>,
105 contents: impl 'static + Send + AsRef<[u8]>,
107 ) -> Result<(), Error> {
108 if resource.should_emit(emit) {
109 self.shared.fs.write(resource.path(self), contents)
117 resource: SharedResource<'_>,
118 contents: impl 'static + Send + AsRef<str> + AsRef<[u8]>,
121 ) -> Result<(), Error> {
123 let contents = contents.as_ref();
124 let contents = if resource.extension() == Some(OsStr::new("css")) {
125 minifier::css::minify(contents)
127 Error::new(format!("failed to minify CSS file: {}", e), resource.path(self))
131 minifier::js::minify(contents).to_string()
133 self.write_shared(resource, contents, emit)
135 self.write_shared(resource, contents, emit)
140 pub(super) fn write_shared(
141 cx: &mut Context<'_>,
143 search_index: String,
144 options: &RenderOptions,
145 ) -> Result<(), Error> {
146 // Write out the shared files. Note that these are shared among all rustdoc
147 // docs placed in the output directory, so this needs to be a synchronized
148 // operation with respect to all other rustdocs running around.
149 let lock_file = cx.dst.join(".lock");
150 let _lock = try_err!(flock::Lock::new(&lock_file, true, true, true), &lock_file);
152 // Minified resources are usually toolchain resources. If they're not, they should use `cx.write_minify` directly.
154 basename: &'static str,
155 contents: impl 'static + Send + AsRef<str> + AsRef<[u8]>,
157 options: &RenderOptions,
158 ) -> Result<(), Error> {
160 SharedResource::ToolchainSpecific { basename },
162 options.enable_minification,
167 // Toolchain resources should never be dynamic.
168 let write_toolchain = |p: &'static _, c: &'static _| {
169 cx.write_shared(SharedResource::ToolchainSpecific { basename: p }, c, &options.emit)
172 // Crate resources should always be dynamic.
173 let write_crate = |p: &_, make_content: &dyn Fn() -> Result<Vec<u8>, Error>| {
174 let content = make_content()?;
175 cx.write_shared(SharedResource::InvocationSpecific { basename: p }, content, &options.emit)
178 // Given "foo.svg", return e.g. "url(\"foo1.58.0.svg\")"
179 fn ver_url(cx: &Context<'_>, basename: &'static str) -> String {
182 SharedResource::ToolchainSpecific { basename }
191 // We use the AUTOREPLACE mechanism to inject into our static JS and CSS certain
192 // values that are only known at doc build time. Since this mechanism is somewhat
193 // surprising when reading the code, please limit it to rustdoc.css.
196 static_files::RUSTDOC_CSS
198 "/* AUTOREPLACE: */url(\"toggle-minus.svg\")",
199 &ver_url(cx, "toggle-minus.svg"),
201 .replace("/* AUTOREPLACE: */url(\"toggle-plus.svg\")", &ver_url(cx, "toggle-plus.svg"))
202 .replace("/* AUTOREPLACE: */url(\"down-arrow.svg\")", &ver_url(cx, "down-arrow.svg")),
207 // Add all the static files. These may already exist, but we just
208 // overwrite them anyway to make sure that they're fresh and up-to-date.
209 write_minify("settings.css", static_files::SETTINGS_CSS, cx, options)?;
210 write_minify("noscript.css", static_files::NOSCRIPT_CSS, cx, options)?;
212 // To avoid "light.css" to be overwritten, we'll first run over the received themes and only
213 // then we'll run over the "official" styles.
214 let mut themes: FxHashSet<String> = FxHashSet::default();
216 for entry in &cx.shared.style_files {
217 let theme = entry.basename()?;
219 try_none!(try_none!(entry.path.extension(), &entry.path).to_str(), &entry.path);
221 // Handle the official themes
222 match theme.as_str() {
223 "light" => write_minify("light.css", static_files::themes::LIGHT, cx, options)?,
224 "dark" => write_minify("dark.css", static_files::themes::DARK, cx, options)?,
225 "ayu" => write_minify("ayu.css", static_files::themes::AYU, cx, options)?,
227 // Handle added third-party themes
228 let filename = format!("{}.{}", theme, extension);
229 write_crate(&filename, &|| Ok(try_err!(fs::read(&entry.path), &entry.path)))?;
233 themes.insert(theme.to_owned());
236 if (*cx.shared).layout.logo.is_empty() {
237 write_toolchain("rust-logo.svg", static_files::RUST_LOGO_SVG)?;
239 if (*cx.shared).layout.favicon.is_empty() {
240 write_toolchain("favicon.svg", static_files::RUST_FAVICON_SVG)?;
241 write_toolchain("favicon-16x16.png", static_files::RUST_FAVICON_PNG_16)?;
242 write_toolchain("favicon-32x32.png", static_files::RUST_FAVICON_PNG_32)?;
244 write_toolchain("wheel.svg", static_files::WHEEL_SVG)?;
245 write_toolchain("clipboard.svg", static_files::CLIPBOARD_SVG)?;
246 write_toolchain("down-arrow.svg", static_files::DOWN_ARROW_SVG)?;
247 write_toolchain("toggle-minus.svg", static_files::TOGGLE_MINUS_PNG)?;
248 write_toolchain("toggle-plus.svg", static_files::TOGGLE_PLUS_PNG)?;
250 let mut themes: Vec<&String> = themes.iter().collect();
253 write_minify("main.js", static_files::MAIN_JS, cx, options)?;
254 write_minify("search.js", static_files::SEARCH_JS, cx, options)?;
255 write_minify("settings.js", static_files::SETTINGS_JS, cx, options)?;
257 if cx.include_sources {
258 write_minify("source-script.js", static_files::sidebar::SOURCE_SCRIPT, cx, options)?;
261 write_minify("storage.js", static_files::STORAGE_JS, cx, options)?;
263 if cx.shared.layout.scrape_examples_extension {
265 SharedResource::InvocationSpecific { basename: "scrape-examples.js" },
266 static_files::SCRAPE_EXAMPLES_JS,
267 options.enable_minification,
272 if let Some(ref css) = cx.shared.layout.css_file_extension {
273 let buffer = try_err!(fs::read_to_string(css), css);
274 // This varies based on the invocation, so it can't go through the write_minify wrapper.
276 SharedResource::InvocationSpecific { basename: "theme.css" },
278 options.enable_minification,
282 write_minify("normalize.css", static_files::NORMALIZE_CSS, cx, options)?;
283 for (name, contents) in &*FILES_UNVERSIONED {
284 cx.write_shared(SharedResource::Unversioned { name }, contents, &options.emit)?;
287 /// Read a file and return all lines that match the `"{crate}":{data},` format,
288 /// and return a tuple `(Vec<DataString>, Vec<CrateNameString>)`.
290 /// This forms the payload of files that look like this:
294 /// "{crate1}":{data},
295 /// "{crate2}":{data}
300 /// The file needs to be formatted so that *only crate data lines start with `"`*.
301 fn collect(path: &Path, krate: &str) -> io::Result<(Vec<String>, Vec<String>)> {
302 let mut ret = Vec::new();
303 let mut krates = Vec::new();
306 let prefix = format!("\"{}\"", krate);
307 for line in BufReader::new(File::open(path)?).lines() {
309 if !line.starts_with('"') {
312 if line.starts_with(&prefix) {
315 if line.ends_with(',') {
316 ret.push(line[..line.len() - 1].to_string());
318 // No comma (it's the case for the last added crate line)
319 ret.push(line.to_string());
323 .find(|s| !s.is_empty())
324 .map(|s| s.to_owned())
325 .unwrap_or_else(String::new),
332 /// Read a file and return all lines that match the <code>"{crate}":{data},\</code> format,
333 /// and return a tuple `(Vec<DataString>, Vec<CrateNameString>)`.
335 /// This forms the payload of files that look like this:
338 /// var data = JSON.parse('{\
339 /// "{crate1}":{data},\
340 /// "{crate2}":{data}\
345 /// The file needs to be formatted so that *only crate data lines start with `"`*.
346 fn collect_json(path: &Path, krate: &str) -> io::Result<(Vec<String>, Vec<String>)> {
347 let mut ret = Vec::new();
348 let mut krates = Vec::new();
351 let prefix = format!("\"{}\"", krate);
352 for line in BufReader::new(File::open(path)?).lines() {
354 if !line.starts_with('"') {
357 if line.starts_with(&prefix) {
360 if line.ends_with(",\\") {
361 ret.push(line[..line.len() - 2].to_string());
363 // Ends with "\\" (it's the case for the last added crate line)
364 ret.push(line[..line.len() - 1].to_string());
368 .find(|s| !s.is_empty())
369 .map(|s| s.to_owned())
370 .unwrap_or_else(String::new),
377 use std::ffi::OsString;
382 children: FxHashMap<OsString, Hierarchy>,
383 elems: FxHashSet<OsString>,
387 fn new(elem: OsString) -> Hierarchy {
388 Hierarchy { elem, children: FxHashMap::default(), elems: FxHashSet::default() }
391 fn to_json_string(&self) -> String {
392 let mut subs: Vec<&Hierarchy> = self.children.values().collect();
393 subs.sort_unstable_by(|a, b| a.elem.cmp(&b.elem));
397 .map(|s| format!("\"{}\"", s.to_str().expect("invalid osstring conversion")))
398 .collect::<Vec<_>>();
399 files.sort_unstable();
400 let subs = subs.iter().map(|s| s.to_json_string()).collect::<Vec<_>>().join(",");
401 let dirs = if subs.is_empty() && files.is_empty() {
404 format!(",[{}]", subs)
406 let files = files.join(",");
407 let files = if files.is_empty() { String::new() } else { format!(",[{}]", files) };
409 "[\"{name}\"{dirs}{files}]",
410 name = self.elem.to_str().expect("invalid osstring conversion"),
417 if cx.include_sources {
418 let mut hierarchy = Hierarchy::new(OsString::new());
423 .filter_map(|p| p.0.strip_prefix(&cx.shared.src_root).ok())
425 let mut h = &mut hierarchy;
426 let mut elems = source
428 .filter_map(|s| match s {
429 Component::Normal(s) => Some(s.to_owned()),
434 let cur_elem = elems.next().expect("empty file path");
435 if elems.peek().is_none() {
436 h.elems.insert(cur_elem);
439 let e = cur_elem.clone();
440 h = h.children.entry(cur_elem.clone()).or_insert_with(|| Hierarchy::new(e));
445 let dst = cx.dst.join(&format!("source-files{}.js", cx.shared.resource_suffix));
446 let make_sources = || {
447 let (mut all_sources, _krates) =
448 try_err!(collect_json(&dst, krate.name(cx.tcx()).as_str()), &dst);
449 all_sources.push(format!(
451 &krate.name(cx.tcx()),
454 // All these `replace` calls are because we have to go through JS string for JSON content.
455 .replace('\\', r"\\")
456 .replace('\'', r"\'")
457 // We need to escape double quotes for the JSON.
458 .replace("\\\"", "\\\\\"")
461 let mut v = String::from("var sourcesIndex = JSON.parse('{\\\n");
462 v.push_str(&all_sources.join(",\\\n"));
463 v.push_str("\\\n}');\ncreateSourceSidebar();\n");
466 write_crate("source-files.js", &make_sources)?;
469 // Update the search index and crate list.
470 let dst = cx.dst.join(&format!("search-index{}.js", cx.shared.resource_suffix));
471 let (mut all_indexes, mut krates) =
472 try_err!(collect_json(&dst, krate.name(cx.tcx()).as_str()), &dst);
473 all_indexes.push(search_index);
474 krates.push(krate.name(cx.tcx()).to_string());
477 // Sort the indexes by crate so the file will be generated identically even
478 // with rustdoc running in parallel.
480 write_crate("search-index.js", &|| {
481 let mut v = String::from("var searchIndex = JSON.parse('{\\\n");
482 v.push_str(&all_indexes.join(",\\\n"));
486 if (typeof window !== 'undefined' && window.initSearch) {window.initSearch(searchIndex)};
487 if (typeof exports !== 'undefined') {exports.searchIndex = searchIndex};
493 write_crate("crates.js", &|| {
494 let krates = krates.iter().map(|k| format!("\"{}\"", k)).join(",");
495 Ok(format!("window.ALL_CRATES = [{}];", krates).into_bytes())
498 if options.enable_index_page {
499 if let Some(index_page) = options.index_page.clone() {
500 let mut md_opts = options.clone();
501 md_opts.output = cx.dst.clone();
502 md_opts.external_html = (*cx.shared).layout.external_html.clone();
504 crate::markdown::render(&index_page, md_opts, cx.shared.edition())
505 .map_err(|e| Error::new(e, &index_page))?;
507 let shared = Rc::clone(&cx.shared);
508 let dst = cx.dst.join("index.html");
509 let page = layout::Page {
510 title: "Index of crates",
513 static_root_path: shared.static_root_path.as_deref(),
514 description: "List of crates",
515 keywords: BASIC_KEYWORDS,
516 resource_suffix: &shared.resource_suffix,
519 let content = format!(
521 <span class=\"in-band\">List of all crates</span>\
522 </h1><ul class=\"crate mod\">{}</ul>",
527 "<li><a class=\"crate mod\" href=\"{}index.html\">{}</a></li>",
528 ensure_trailing_slash(s),
534 let v = layout::render(&shared.layout, &page, "", content, &shared.style_files);
535 shared.fs.write(dst, v)?;
539 // Update the list of all implementors for traits
540 let dst = cx.dst.join("implementors");
541 let cache = cx.cache();
542 for (&did, imps) in &cache.implementors {
543 // Private modules can leak through to this phase of rustdoc, which
544 // could contain implementations for otherwise private types. In some
545 // rare cases we could find an implementation for an item which wasn't
546 // indexed, so we just skip this step in that case.
548 // FIXME: this is a vague explanation for why this can't be a `get`, in
549 // theory it should be...
550 let (remote_path, remote_item_type) = match cache.exact_paths.get(&did) {
551 Some(p) => match cache.paths.get(&did).or_else(|| cache.external_paths.get(&did)) {
552 Some((_, t)) => (p, t),
555 None => match cache.external_paths.get(&did) {
556 Some((p, t)) => (p, t),
567 impl Serialize for Implementor {
568 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
572 let mut seq = serializer.serialize_seq(None)?;
573 seq.serialize_element(&self.text)?;
575 seq.serialize_element(&1)?;
576 seq.serialize_element(&self.types)?;
582 let implementors = imps
585 // If the trait and implementation are in the same crate, then
586 // there's no need to emit information about it (there's inlining
587 // going on). If they're in different crates then the crate defining
588 // the trait will be interested in our implementation.
590 // If the implementation is from another crate then that crate
592 if imp.impl_item.item_id.krate() == did.krate || !imp.impl_item.item_id.is_local() {
596 text: imp.inner_impl().print(false, cx).to_string(),
597 synthetic: imp.inner_impl().kind.is_auto(),
598 types: collect_paths_for_type(imp.inner_impl().for_.clone(), cache),
602 .collect::<Vec<_>>();
604 // Only create a js file if we have impls to add to it. If the trait is
605 // documented locally though we always create the file to avoid dead
607 if implementors.is_empty() && !cache.paths.contains_key(&did) {
611 let implementors = format!(
613 krate.name(cx.tcx()),
614 serde_json::to_string(&implementors).expect("failed serde conversion"),
617 let mut mydst = dst.clone();
618 for part in &remote_path[..remote_path.len() - 1] {
619 mydst.push(part.to_string());
621 cx.shared.ensure_dir(&mydst)?;
622 mydst.push(&format!("{}.{}.js", remote_item_type, remote_path[remote_path.len() - 1]));
624 let (mut all_implementors, _) =
625 try_err!(collect(&mydst, krate.name(cx.tcx()).as_str()), &mydst);
626 all_implementors.push(implementors);
627 // Sort the implementors by crate so the file will be generated
628 // identically even with rustdoc running in parallel.
629 all_implementors.sort();
631 let mut v = String::from("(function() {var implementors = {\n");
632 v.push_str(&all_implementors.join(",\n"));
635 "if (window.register_implementors) {\
636 window.register_implementors(implementors);\
638 window.pending_implementors = implementors;\
642 cx.shared.fs.write(mydst, v)?;