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::RenderOptions;
17 use crate::docfs::{DocFS, PathError};
18 use crate::error::Error;
19 use crate::formats::FormatRenderer;
20 use crate::html::{layout, static_files};
22 crate static FILES_UNVERSIONED: Lazy<FxHashMap<&str, &[u8]>> = Lazy::new(|| {
24 "FiraSans-Regular.woff2" => static_files::fira_sans::REGULAR2,
25 "FiraSans-Medium.woff2" => static_files::fira_sans::MEDIUM2,
26 "FiraSans-Regular.woff" => static_files::fira_sans::REGULAR,
27 "FiraSans-Medium.woff" => static_files::fira_sans::MEDIUM,
28 "FiraSans-LICENSE.txt" => static_files::fira_sans::LICENSE,
29 "SourceSerifPro-Regular.ttf.woff" => static_files::source_serif_pro::REGULAR,
30 "SourceSerifPro-Bold.ttf.woff" => static_files::source_serif_pro::BOLD,
31 "SourceSerifPro-It.ttf.woff" => static_files::source_serif_pro::ITALIC,
32 "SourceSerifPro-LICENSE.md" => static_files::source_serif_pro::LICENSE,
33 "SourceCodePro-Regular.ttf.woff" => static_files::source_code_pro::REGULAR,
34 "SourceCodePro-Semibold.ttf.woff" => static_files::source_code_pro::SEMIBOLD,
35 "SourceCodePro-It.ttf.woff" => static_files::source_code_pro::ITALIC,
36 "SourceCodePro-LICENSE.txt" => static_files::source_code_pro::LICENSE,
37 "LICENSE-MIT.txt" => static_files::LICENSE_MIT,
38 "LICENSE-APACHE.txt" => static_files::LICENSE_APACHE,
39 "COPYRIGHT.txt" => static_files::COPYRIGHT,
43 pub(super) fn write_shared(
47 options: &RenderOptions,
48 ) -> Result<(), Error> {
49 // Write out the shared files. Note that these are shared among all rustdoc
50 // docs placed in the output directory, so this needs to be a synchronized
51 // operation with respect to all other rustdocs running around.
52 let lock_file = cx.dst.join(".lock");
53 let _lock = try_err!(flock::Lock::new(&lock_file, true, true, true), &lock_file);
55 // Add all the static files. These may already exist, but we just
56 // overwrite them anyway to make sure that they're fresh and up-to-date.
60 cx.path("rustdoc.css"),
61 static_files::RUSTDOC_CSS,
62 options.enable_minification,
66 cx.path("settings.css"),
67 static_files::SETTINGS_CSS,
68 options.enable_minification,
72 cx.path("noscript.css"),
73 static_files::NOSCRIPT_CSS,
74 options.enable_minification,
77 // To avoid "light.css" to be overwritten, we'll first run over the received themes and only
78 // then we'll run over the "official" styles.
79 let mut themes: FxHashSet<String> = FxHashSet::default();
81 for entry in &cx.shared.style_files {
82 let theme = try_none!(try_none!(entry.path.file_stem(), &entry.path).to_str(), &entry.path);
84 try_none!(try_none!(entry.path.extension(), &entry.path).to_str(), &entry.path);
86 // Handle the official themes
88 "light" => write_minify(
91 static_files::themes::LIGHT,
92 options.enable_minification,
94 "dark" => write_minify(
97 static_files::themes::DARK,
98 options.enable_minification,
100 "ayu" => write_minify(
103 static_files::themes::AYU,
104 options.enable_minification,
107 // Handle added third-party themes
108 let content = try_err!(fs::read(&entry.path), &entry.path);
111 .write(cx.path(&format!("{}.{}", theme, extension)), content.as_slice())?;
115 themes.insert(theme.to_owned());
118 let write = |p, c| cx.shared.fs.write(p, c);
119 if (*cx.shared).layout.logo.is_empty() {
120 write(cx.path("rust-logo.png"), static_files::RUST_LOGO)?;
122 if (*cx.shared).layout.favicon.is_empty() {
123 write(cx.path("favicon.svg"), static_files::RUST_FAVICON_SVG)?;
124 write(cx.path("favicon-16x16.png"), static_files::RUST_FAVICON_PNG_16)?;
125 write(cx.path("favicon-32x32.png"), static_files::RUST_FAVICON_PNG_32)?;
127 write(cx.path("brush.svg"), static_files::BRUSH_SVG)?;
128 write(cx.path("wheel.svg"), static_files::WHEEL_SVG)?;
129 write(cx.path("down-arrow.svg"), static_files::DOWN_ARROW_SVG)?;
131 let mut themes: Vec<&String> = themes.iter().collect();
133 // To avoid theme switch latencies as much as possible, we put everything theme related
134 // at the beginning of the html files into another js file.
135 let theme_js = format!(
136 r#"var themes = document.getElementById("theme-choices");
137 var themePicker = document.getElementById("theme-picker");
139 function showThemeButtonState() {{
140 themes.style.display = "block";
141 themePicker.style.borderBottomRightRadius = "0";
142 themePicker.style.borderBottomLeftRadius = "0";
145 function hideThemeButtonState() {{
146 themes.style.display = "none";
147 themePicker.style.borderBottomRightRadius = "3px";
148 themePicker.style.borderBottomLeftRadius = "3px";
151 function switchThemeButtonState() {{
152 if (themes.style.display === "block") {{
153 hideThemeButtonState();
155 showThemeButtonState();
159 function handleThemeButtonsBlur(e) {{
160 var active = document.activeElement;
161 var related = e.relatedTarget;
163 if (active.id !== "theme-picker" &&
164 (!active.parentNode || active.parentNode.id !== "theme-choices") &&
166 (related.id !== "theme-picker" &&
167 (!related.parentNode || related.parentNode.id !== "theme-choices")))) {{
168 hideThemeButtonState();
172 themePicker.onclick = switchThemeButtonState;
173 themePicker.onblur = handleThemeButtonsBlur;
174 {}.forEach(function(item) {{
175 var but = document.createElement("button");
176 but.textContent = item;
177 but.onclick = function(el) {{
178 switchTheme(currentTheme, mainTheme, item, true);
179 useSystemTheme(false);
181 but.onblur = handleThemeButtonsBlur;
182 themes.appendChild(but);
184 serde_json::to_string(&themes).unwrap()
187 write_minify(&cx.shared.fs, cx.path("theme.js"), &theme_js, options.enable_minification)?;
191 static_files::MAIN_JS,
192 options.enable_minification,
196 cx.path("settings.js"),
197 static_files::SETTINGS_JS,
198 options.enable_minification,
200 if cx.shared.include_sources {
203 cx.path("source-script.js"),
204 static_files::sidebar::SOURCE_SCRIPT,
205 options.enable_minification,
212 cx.path("storage.js"),
214 "var resourcesSuffix = \"{}\";{}",
215 cx.shared.resource_suffix,
216 static_files::STORAGE_JS
218 options.enable_minification,
222 if let Some(ref css) = cx.shared.layout.css_file_extension {
223 let out = cx.path("theme.css");
224 let buffer = try_err!(fs::read_to_string(css), css);
225 if !options.enable_minification {
226 cx.shared.fs.write(&out, &buffer)?;
228 write_minify(&cx.shared.fs, out, &buffer, options.enable_minification)?;
233 cx.path("normalize.css"),
234 static_files::NORMALIZE_CSS,
235 options.enable_minification,
237 for (file, contents) in &*FILES_UNVERSIONED {
238 write(cx.dst.join(file), contents)?;
241 fn collect(path: &Path, krate: &str, key: &str) -> io::Result<(Vec<String>, Vec<String>)> {
242 let mut ret = Vec::new();
243 let mut krates = Vec::new();
246 let prefix = format!(r#"{}["{}"]"#, key, krate);
247 for line in BufReader::new(File::open(path)?).lines() {
249 if !line.starts_with(key) {
252 if line.starts_with(&prefix) {
255 ret.push(line.to_string());
257 line[key.len() + 2..]
260 .map(|s| s.to_owned())
261 .unwrap_or_else(String::new),
268 fn collect_json(path: &Path, krate: &str) -> io::Result<(Vec<String>, Vec<String>)> {
269 let mut ret = Vec::new();
270 let mut krates = Vec::new();
273 let prefix = format!("\"{}\"", krate);
274 for line in BufReader::new(File::open(path)?).lines() {
276 if !line.starts_with('"') {
279 if line.starts_with(&prefix) {
282 if line.ends_with(",\\") {
283 ret.push(line[..line.len() - 2].to_string());
285 // Ends with "\\" (it's the case for the last added crate line)
286 ret.push(line[..line.len() - 1].to_string());
290 .find(|s| !s.is_empty())
291 .map(|s| s.to_owned())
292 .unwrap_or_else(String::new),
299 use std::ffi::OsString;
304 children: FxHashMap<OsString, Hierarchy>,
305 elems: FxHashSet<OsString>,
309 fn new(elem: OsString) -> Hierarchy {
310 Hierarchy { elem, children: FxHashMap::default(), elems: FxHashSet::default() }
313 fn to_json_string(&self) -> String {
314 let mut subs: Vec<&Hierarchy> = self.children.values().collect();
315 subs.sort_unstable_by(|a, b| a.elem.cmp(&b.elem));
319 .map(|s| format!("\"{}\"", s.to_str().expect("invalid osstring conversion")))
320 .collect::<Vec<_>>();
321 files.sort_unstable();
322 let subs = subs.iter().map(|s| s.to_json_string()).collect::<Vec<_>>().join(",");
324 if subs.is_empty() { String::new() } else { format!(",\"dirs\":[{}]", subs) };
325 let files = files.join(",");
327 if files.is_empty() { String::new() } else { format!(",\"files\":[{}]", files) };
329 "{{\"name\":\"{name}\"{dirs}{files}}}",
330 name = self.elem.to_str().expect("invalid osstring conversion"),
337 if cx.shared.include_sources {
338 let mut hierarchy = Hierarchy::new(OsString::new());
343 .filter_map(|p| p.0.strip_prefix(&cx.shared.src_root).ok())
345 let mut h = &mut hierarchy;
346 let mut elems = source
348 .filter_map(|s| match s {
349 Component::Normal(s) => Some(s.to_owned()),
354 let cur_elem = elems.next().expect("empty file path");
355 if elems.peek().is_none() {
356 h.elems.insert(cur_elem);
359 let e = cur_elem.clone();
360 h = h.children.entry(cur_elem.clone()).or_insert_with(|| Hierarchy::new(e));
365 let dst = cx.dst.join(&format!("source-files{}.js", cx.shared.resource_suffix));
366 let (mut all_sources, _krates) =
367 try_err!(collect(&dst, &krate.name.as_str(), "sourcesIndex"), &dst);
368 all_sources.push(format!(
369 "sourcesIndex[\"{}\"] = {};",
371 hierarchy.to_json_string()
375 "var N = null;var sourcesIndex = {{}};\n{}\ncreateSourceSidebar();\n",
376 all_sources.join("\n")
378 cx.shared.fs.write(&dst, v.as_bytes())?;
381 // Update the search index and crate list.
382 let dst = cx.dst.join(&format!("search-index{}.js", cx.shared.resource_suffix));
383 let (mut all_indexes, mut krates) = try_err!(collect_json(&dst, &krate.name.as_str()), &dst);
384 all_indexes.push(search_index);
385 krates.push(krate.name.to_string());
388 // Sort the indexes by crate so the file will be generated identically even
389 // with rustdoc running in parallel.
392 let mut v = String::from("var searchIndex = JSON.parse('{\\\n");
393 v.push_str(&all_indexes.join(",\\\n"));
394 v.push_str("\\\n}');\ninitSearch(searchIndex);");
395 cx.shared.fs.write(&dst, &v)?;
398 let crate_list_dst = cx.dst.join(&format!("crates{}.js", cx.shared.resource_suffix));
400 format!("window.ALL_CRATES = [{}];", krates.iter().map(|k| format!("\"{}\"", k)).join(","));
401 cx.shared.fs.write(&crate_list_dst, &crate_list)?;
403 if options.enable_index_page {
404 if let Some(index_page) = options.index_page.clone() {
405 let mut md_opts = options.clone();
406 md_opts.output = cx.dst.clone();
407 md_opts.external_html = (*cx.shared).layout.external_html.clone();
409 crate::markdown::render(&index_page, md_opts, cx.shared.edition)
410 .map_err(|e| Error::new(e, &index_page))?;
412 let dst = cx.dst.join("index.html");
413 let page = layout::Page {
414 title: "Index of crates",
417 static_root_path: cx.shared.static_root_path.as_deref(),
418 description: "List of crates",
419 keywords: BASIC_KEYWORDS,
420 resource_suffix: &cx.shared.resource_suffix,
422 static_extra_scripts: &[],
425 let content = format!(
427 <span class=\"in-band\">List of all crates</span>\
428 </h1><ul class=\"crate mod\">{}</ul>",
433 "<li><a class=\"crate mod\" href=\"{}index.html\">{}</a></li>",
434 ensure_trailing_slash(s),
440 let v = layout::render(&cx.shared.layout, &page, "", content, &cx.shared.style_files);
441 cx.shared.fs.write(&dst, v.as_bytes())?;
445 // Update the list of all implementors for traits
446 let dst = cx.dst.join("implementors");
447 for (&did, imps) in &cx.cache.implementors {
448 // Private modules can leak through to this phase of rustdoc, which
449 // could contain implementations for otherwise private types. In some
450 // rare cases we could find an implementation for an item which wasn't
451 // indexed, so we just skip this step in that case.
453 // FIXME: this is a vague explanation for why this can't be a `get`, in
454 // theory it should be...
455 let &(ref remote_path, remote_item_type) = match cx.cache.paths.get(&did) {
457 None => match cx.cache.external_paths.get(&did) {
470 let implementors = imps
473 // If the trait and implementation are in the same crate, then
474 // there's no need to emit information about it (there's inlining
475 // going on). If they're in different crates then the crate defining
476 // the trait will be interested in our implementation.
478 // If the implementation is from another crate then that crate
480 if imp.impl_item.def_id.krate == did.krate || !imp.impl_item.def_id.is_local() {
484 text: imp.inner_impl().print(cx.cache(), false).to_string(),
485 synthetic: imp.inner_impl().synthetic,
486 types: collect_paths_for_type(imp.inner_impl().for_.clone(), cx.cache()),
490 .collect::<Vec<_>>();
492 // Only create a js file if we have impls to add to it. If the trait is
493 // documented locally though we always create the file to avoid dead
495 if implementors.is_empty() && !cx.cache.paths.contains_key(&did) {
499 let implementors = format!(
500 r#"implementors["{}"] = {};"#,
502 serde_json::to_string(&implementors).unwrap()
505 let mut mydst = dst.clone();
506 for part in &remote_path[..remote_path.len() - 1] {
509 cx.shared.ensure_dir(&mydst)?;
510 mydst.push(&format!("{}.{}.js", remote_item_type, remote_path[remote_path.len() - 1]));
512 let (mut all_implementors, _) =
513 try_err!(collect(&mydst, &krate.name.as_str(), "implementors"), &mydst);
514 all_implementors.push(implementors);
515 // Sort the implementors by crate so the file will be generated
516 // identically even with rustdoc running in parallel.
517 all_implementors.sort();
519 let mut v = String::from("(function() {var implementors = {};\n");
520 for implementor in &all_implementors {
521 writeln!(v, "{}", *implementor).unwrap();
524 "if (window.register_implementors) {\
525 window.register_implementors(implementors);\
527 window.pending_implementors = implementors;\
531 cx.shared.fs.write(&mydst, &v)?;
540 enable_minification: bool,
541 ) -> Result<(), Error> {
542 if enable_minification {
543 if dst.extension() == Some(&OsStr::new("css")) {
544 let res = try_none!(minifier::css::minify(contents).ok(), &dst);
545 fs.write(dst, res.as_bytes())
547 fs.write(dst, minifier::js::minify(contents).as_bytes())
550 fs.write(dst, contents.as_bytes())