]> git.lizzy.rs Git - rust.git/blob - src/librustdoc/html/render/write_shared.rs
Auto merge of #82417 - the8472:fix-copy_file_range-append, r=m-ou-se
[rust.git] / src / librustdoc / html / render / write_shared.rs
1 use std::ffi::OsStr;
2 use std::fmt::Write;
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};
8
9 use itertools::Itertools;
10 use rustc_data_structures::flock;
11 use rustc_data_structures::fx::{FxHashMap, FxHashSet};
12 use serde::Serialize;
13
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};
21
22 crate static FILES_UNVERSIONED: Lazy<FxHashMap<&str, &[u8]>> = Lazy::new(|| {
23     map! {
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.woff" => static_files::source_code_pro::REGULAR,
34         "SourceCodePro-Semibold.woff" => static_files::source_code_pro::SEMIBOLD,
35         "SourceCodePro-LICENSE.txt" => static_files::source_code_pro::LICENSE,
36         "LICENSE-MIT.txt" => static_files::LICENSE_MIT,
37         "LICENSE-APACHE.txt" => static_files::LICENSE_APACHE,
38         "COPYRIGHT.txt" => static_files::COPYRIGHT,
39     }
40 });
41
42 pub(super) fn write_shared(
43     cx: &Context<'_>,
44     krate: &Crate,
45     search_index: String,
46     options: &RenderOptions,
47 ) -> Result<(), Error> {
48     // Write out the shared files. Note that these are shared among all rustdoc
49     // docs placed in the output directory, so this needs to be a synchronized
50     // operation with respect to all other rustdocs running around.
51     let lock_file = cx.dst.join(".lock");
52     let _lock = try_err!(flock::Lock::new(&lock_file, true, true, true), &lock_file);
53
54     // Add all the static files. These may already exist, but we just
55     // overwrite them anyway to make sure that they're fresh and up-to-date.
56
57     write_minify(
58         &cx.shared.fs,
59         cx.path("rustdoc.css"),
60         static_files::RUSTDOC_CSS,
61         options.enable_minification,
62     )?;
63     write_minify(
64         &cx.shared.fs,
65         cx.path("settings.css"),
66         static_files::SETTINGS_CSS,
67         options.enable_minification,
68     )?;
69     write_minify(
70         &cx.shared.fs,
71         cx.path("noscript.css"),
72         static_files::NOSCRIPT_CSS,
73         options.enable_minification,
74     )?;
75
76     // To avoid "light.css" to be overwritten, we'll first run over the received themes and only
77     // then we'll run over the "official" styles.
78     let mut themes: FxHashSet<String> = FxHashSet::default();
79
80     for entry in &cx.shared.style_files {
81         let theme = try_none!(try_none!(entry.path.file_stem(), &entry.path).to_str(), &entry.path);
82         let extension =
83             try_none!(try_none!(entry.path.extension(), &entry.path).to_str(), &entry.path);
84
85         // Handle the official themes
86         match theme {
87             "light" => write_minify(
88                 &cx.shared.fs,
89                 cx.path("light.css"),
90                 static_files::themes::LIGHT,
91                 options.enable_minification,
92             )?,
93             "dark" => write_minify(
94                 &cx.shared.fs,
95                 cx.path("dark.css"),
96                 static_files::themes::DARK,
97                 options.enable_minification,
98             )?,
99             "ayu" => write_minify(
100                 &cx.shared.fs,
101                 cx.path("ayu.css"),
102                 static_files::themes::AYU,
103                 options.enable_minification,
104             )?,
105             _ => {
106                 // Handle added third-party themes
107                 let content = try_err!(fs::read(&entry.path), &entry.path);
108                 cx.shared
109                     .fs
110                     .write(cx.path(&format!("{}.{}", theme, extension)), content.as_slice())?;
111             }
112         };
113
114         themes.insert(theme.to_owned());
115     }
116
117     let write = |p, c| cx.shared.fs.write(p, c);
118     if (*cx.shared).layout.logo.is_empty() {
119         write(cx.path("rust-logo.png"), static_files::RUST_LOGO)?;
120     }
121     if (*cx.shared).layout.favicon.is_empty() {
122         write(cx.path("favicon.svg"), static_files::RUST_FAVICON_SVG)?;
123         write(cx.path("favicon-16x16.png"), static_files::RUST_FAVICON_PNG_16)?;
124         write(cx.path("favicon-32x32.png"), static_files::RUST_FAVICON_PNG_32)?;
125     }
126     write(cx.path("brush.svg"), static_files::BRUSH_SVG)?;
127     write(cx.path("wheel.svg"), static_files::WHEEL_SVG)?;
128     write(cx.path("down-arrow.svg"), static_files::DOWN_ARROW_SVG)?;
129
130     let mut themes: Vec<&String> = themes.iter().collect();
131     themes.sort();
132     // To avoid theme switch latencies as much as possible, we put everything theme related
133     // at the beginning of the html files into another js file.
134     let theme_js = format!(
135         r#"var themes = document.getElementById("theme-choices");
136 var themePicker = document.getElementById("theme-picker");
137
138 function showThemeButtonState() {{
139     themes.style.display = "block";
140     themePicker.style.borderBottomRightRadius = "0";
141     themePicker.style.borderBottomLeftRadius = "0";
142 }}
143
144 function hideThemeButtonState() {{
145     themes.style.display = "none";
146     themePicker.style.borderBottomRightRadius = "3px";
147     themePicker.style.borderBottomLeftRadius = "3px";
148 }}
149
150 function switchThemeButtonState() {{
151     if (themes.style.display === "block") {{
152         hideThemeButtonState();
153     }} else {{
154         showThemeButtonState();
155     }}
156 }};
157
158 function handleThemeButtonsBlur(e) {{
159     var active = document.activeElement;
160     var related = e.relatedTarget;
161
162     if (active.id !== "theme-picker" &&
163         (!active.parentNode || active.parentNode.id !== "theme-choices") &&
164         (!related ||
165          (related.id !== "theme-picker" &&
166           (!related.parentNode || related.parentNode.id !== "theme-choices")))) {{
167         hideThemeButtonState();
168     }}
169 }}
170
171 themePicker.onclick = switchThemeButtonState;
172 themePicker.onblur = handleThemeButtonsBlur;
173 {}.forEach(function(item) {{
174     var but = document.createElement("button");
175     but.textContent = item;
176     but.onclick = function(el) {{
177         switchTheme(currentTheme, mainTheme, item, true);
178         useSystemTheme(false);
179     }};
180     but.onblur = handleThemeButtonsBlur;
181     themes.appendChild(but);
182 }});"#,
183         serde_json::to_string(&themes).unwrap()
184     );
185
186     write_minify(&cx.shared.fs, cx.path("theme.js"), &theme_js, options.enable_minification)?;
187     write_minify(
188         &cx.shared.fs,
189         cx.path("main.js"),
190         static_files::MAIN_JS,
191         options.enable_minification,
192     )?;
193     write_minify(
194         &cx.shared.fs,
195         cx.path("settings.js"),
196         static_files::SETTINGS_JS,
197         options.enable_minification,
198     )?;
199     if cx.shared.include_sources {
200         write_minify(
201             &cx.shared.fs,
202             cx.path("source-script.js"),
203             static_files::sidebar::SOURCE_SCRIPT,
204             options.enable_minification,
205         )?;
206     }
207
208     {
209         write_minify(
210             &cx.shared.fs,
211             cx.path("storage.js"),
212             &format!(
213                 "var resourcesSuffix = \"{}\";{}",
214                 cx.shared.resource_suffix,
215                 static_files::STORAGE_JS
216             ),
217             options.enable_minification,
218         )?;
219     }
220
221     if let Some(ref css) = cx.shared.layout.css_file_extension {
222         let out = cx.path("theme.css");
223         let buffer = try_err!(fs::read_to_string(css), css);
224         if !options.enable_minification {
225             cx.shared.fs.write(&out, &buffer)?;
226         } else {
227             write_minify(&cx.shared.fs, out, &buffer, options.enable_minification)?;
228         }
229     }
230     write_minify(
231         &cx.shared.fs,
232         cx.path("normalize.css"),
233         static_files::NORMALIZE_CSS,
234         options.enable_minification,
235     )?;
236     for (file, contents) in &*FILES_UNVERSIONED {
237         write(cx.dst.join(file), contents)?;
238     }
239
240     fn collect(path: &Path, krate: &str, key: &str) -> io::Result<(Vec<String>, Vec<String>)> {
241         let mut ret = Vec::new();
242         let mut krates = Vec::new();
243
244         if path.exists() {
245             let prefix = format!(r#"{}["{}"]"#, key, krate);
246             for line in BufReader::new(File::open(path)?).lines() {
247                 let line = line?;
248                 if !line.starts_with(key) {
249                     continue;
250                 }
251                 if line.starts_with(&prefix) {
252                     continue;
253                 }
254                 ret.push(line.to_string());
255                 krates.push(
256                     line[key.len() + 2..]
257                         .split('"')
258                         .next()
259                         .map(|s| s.to_owned())
260                         .unwrap_or_else(String::new),
261                 );
262             }
263         }
264         Ok((ret, krates))
265     }
266
267     fn collect_json(path: &Path, krate: &str) -> io::Result<(Vec<String>, Vec<String>)> {
268         let mut ret = Vec::new();
269         let mut krates = Vec::new();
270
271         if path.exists() {
272             let prefix = format!("\"{}\"", krate);
273             for line in BufReader::new(File::open(path)?).lines() {
274                 let line = line?;
275                 if !line.starts_with('"') {
276                     continue;
277                 }
278                 if line.starts_with(&prefix) {
279                     continue;
280                 }
281                 if line.ends_with(",\\") {
282                     ret.push(line[..line.len() - 2].to_string());
283                 } else {
284                     // Ends with "\\" (it's the case for the last added crate line)
285                     ret.push(line[..line.len() - 1].to_string());
286                 }
287                 krates.push(
288                     line.split('"')
289                         .find(|s| !s.is_empty())
290                         .map(|s| s.to_owned())
291                         .unwrap_or_else(String::new),
292                 );
293             }
294         }
295         Ok((ret, krates))
296     }
297
298     use std::ffi::OsString;
299
300     #[derive(Debug)]
301     struct Hierarchy {
302         elem: OsString,
303         children: FxHashMap<OsString, Hierarchy>,
304         elems: FxHashSet<OsString>,
305     }
306
307     impl Hierarchy {
308         fn new(elem: OsString) -> Hierarchy {
309             Hierarchy { elem, children: FxHashMap::default(), elems: FxHashSet::default() }
310         }
311
312         fn to_json_string(&self) -> String {
313             let mut subs: Vec<&Hierarchy> = self.children.values().collect();
314             subs.sort_unstable_by(|a, b| a.elem.cmp(&b.elem));
315             let mut files = self
316                 .elems
317                 .iter()
318                 .map(|s| format!("\"{}\"", s.to_str().expect("invalid osstring conversion")))
319                 .collect::<Vec<_>>();
320             files.sort_unstable();
321             let subs = subs.iter().map(|s| s.to_json_string()).collect::<Vec<_>>().join(",");
322             let dirs =
323                 if subs.is_empty() { String::new() } else { format!(",\"dirs\":[{}]", subs) };
324             let files = files.join(",");
325             let files =
326                 if files.is_empty() { String::new() } else { format!(",\"files\":[{}]", files) };
327             format!(
328                 "{{\"name\":\"{name}\"{dirs}{files}}}",
329                 name = self.elem.to_str().expect("invalid osstring conversion"),
330                 dirs = dirs,
331                 files = files
332             )
333         }
334     }
335
336     if cx.shared.include_sources {
337         let mut hierarchy = Hierarchy::new(OsString::new());
338         for source in cx
339             .shared
340             .local_sources
341             .iter()
342             .filter_map(|p| p.0.strip_prefix(&cx.shared.src_root).ok())
343         {
344             let mut h = &mut hierarchy;
345             let mut elems = source
346                 .components()
347                 .filter_map(|s| match s {
348                     Component::Normal(s) => Some(s.to_owned()),
349                     _ => None,
350                 })
351                 .peekable();
352             loop {
353                 let cur_elem = elems.next().expect("empty file path");
354                 if elems.peek().is_none() {
355                     h.elems.insert(cur_elem);
356                     break;
357                 } else {
358                     let e = cur_elem.clone();
359                     h = h.children.entry(cur_elem.clone()).or_insert_with(|| Hierarchy::new(e));
360                 }
361             }
362         }
363
364         let dst = cx.dst.join(&format!("source-files{}.js", cx.shared.resource_suffix));
365         let (mut all_sources, _krates) =
366             try_err!(collect(&dst, &krate.name.as_str(), "sourcesIndex"), &dst);
367         all_sources.push(format!(
368             "sourcesIndex[\"{}\"] = {};",
369             &krate.name,
370             hierarchy.to_json_string()
371         ));
372         all_sources.sort();
373         let v = format!(
374             "var N = null;var sourcesIndex = {{}};\n{}\ncreateSourceSidebar();\n",
375             all_sources.join("\n")
376         );
377         cx.shared.fs.write(&dst, v.as_bytes())?;
378     }
379
380     // Update the search index and crate list.
381     let dst = cx.dst.join(&format!("search-index{}.js", cx.shared.resource_suffix));
382     let (mut all_indexes, mut krates) = try_err!(collect_json(&dst, &krate.name.as_str()), &dst);
383     all_indexes.push(search_index);
384     krates.push(krate.name.to_string());
385     krates.sort();
386
387     // Sort the indexes by crate so the file will be generated identically even
388     // with rustdoc running in parallel.
389     all_indexes.sort();
390     {
391         let mut v = String::from("var searchIndex = JSON.parse('{\\\n");
392         v.push_str(&all_indexes.join(",\\\n"));
393         v.push_str("\\\n}');\ninitSearch(searchIndex);");
394         cx.shared.fs.write(&dst, &v)?;
395     }
396
397     let crate_list_dst = cx.dst.join(&format!("crates{}.js", cx.shared.resource_suffix));
398     let crate_list =
399         format!("window.ALL_CRATES = [{}];", krates.iter().map(|k| format!("\"{}\"", k)).join(","));
400     cx.shared.fs.write(&crate_list_dst, &crate_list)?;
401
402     if options.enable_index_page {
403         if let Some(index_page) = options.index_page.clone() {
404             let mut md_opts = options.clone();
405             md_opts.output = cx.dst.clone();
406             md_opts.external_html = (*cx.shared).layout.external_html.clone();
407
408             crate::markdown::render(&index_page, md_opts, cx.shared.edition)
409                 .map_err(|e| Error::new(e, &index_page))?;
410         } else {
411             let dst = cx.dst.join("index.html");
412             let page = layout::Page {
413                 title: "Index of crates",
414                 css_class: "mod",
415                 root_path: "./",
416                 static_root_path: cx.shared.static_root_path.as_deref(),
417                 description: "List of crates",
418                 keywords: BASIC_KEYWORDS,
419                 resource_suffix: &cx.shared.resource_suffix,
420                 extra_scripts: &[],
421                 static_extra_scripts: &[],
422             };
423
424             let content = format!(
425                 "<h1 class=\"fqn\">\
426                      <span class=\"in-band\">List of all crates</span>\
427                 </h1><ul class=\"crate mod\">{}</ul>",
428                 krates
429                     .iter()
430                     .map(|s| {
431                         format!(
432                             "<li><a class=\"crate mod\" href=\"{}index.html\">{}</a></li>",
433                             ensure_trailing_slash(s),
434                             s
435                         )
436                     })
437                     .collect::<String>()
438             );
439             let v = layout::render(&cx.shared.layout, &page, "", content, &cx.shared.style_files);
440             cx.shared.fs.write(&dst, v.as_bytes())?;
441         }
442     }
443
444     // Update the list of all implementors for traits
445     let dst = cx.dst.join("implementors");
446     for (&did, imps) in &cx.cache.implementors {
447         // Private modules can leak through to this phase of rustdoc, which
448         // could contain implementations for otherwise private types. In some
449         // rare cases we could find an implementation for an item which wasn't
450         // indexed, so we just skip this step in that case.
451         //
452         // FIXME: this is a vague explanation for why this can't be a `get`, in
453         //        theory it should be...
454         let &(ref remote_path, remote_item_type) = match cx.cache.paths.get(&did) {
455             Some(p) => p,
456             None => match cx.cache.external_paths.get(&did) {
457                 Some(p) => p,
458                 None => continue,
459             },
460         };
461
462         #[derive(Serialize)]
463         struct Implementor {
464             text: String,
465             synthetic: bool,
466             types: Vec<String>,
467         }
468
469         let implementors = imps
470             .iter()
471             .filter_map(|imp| {
472                 // If the trait and implementation are in the same crate, then
473                 // there's no need to emit information about it (there's inlining
474                 // going on). If they're in different crates then the crate defining
475                 // the trait will be interested in our implementation.
476                 //
477                 // If the implementation is from another crate then that crate
478                 // should add it.
479                 if imp.impl_item.def_id.krate == did.krate || !imp.impl_item.def_id.is_local() {
480                     None
481                 } else {
482                     Some(Implementor {
483                         text: imp.inner_impl().print(cx.cache(), false).to_string(),
484                         synthetic: imp.inner_impl().synthetic,
485                         types: collect_paths_for_type(imp.inner_impl().for_.clone(), cx.cache()),
486                     })
487                 }
488             })
489             .collect::<Vec<_>>();
490
491         // Only create a js file if we have impls to add to it. If the trait is
492         // documented locally though we always create the file to avoid dead
493         // links.
494         if implementors.is_empty() && !cx.cache.paths.contains_key(&did) {
495             continue;
496         }
497
498         let implementors = format!(
499             r#"implementors["{}"] = {};"#,
500             krate.name,
501             serde_json::to_string(&implementors).unwrap()
502         );
503
504         let mut mydst = dst.clone();
505         for part in &remote_path[..remote_path.len() - 1] {
506             mydst.push(part);
507         }
508         cx.shared.ensure_dir(&mydst)?;
509         mydst.push(&format!("{}.{}.js", remote_item_type, remote_path[remote_path.len() - 1]));
510
511         let (mut all_implementors, _) =
512             try_err!(collect(&mydst, &krate.name.as_str(), "implementors"), &mydst);
513         all_implementors.push(implementors);
514         // Sort the implementors by crate so the file will be generated
515         // identically even with rustdoc running in parallel.
516         all_implementors.sort();
517
518         let mut v = String::from("(function() {var implementors = {};\n");
519         for implementor in &all_implementors {
520             writeln!(v, "{}", *implementor).unwrap();
521         }
522         v.push_str(
523             "if (window.register_implementors) {\
524                  window.register_implementors(implementors);\
525              } else {\
526                  window.pending_implementors = implementors;\
527              }",
528         );
529         v.push_str("})()");
530         cx.shared.fs.write(&mydst, &v)?;
531     }
532     Ok(())
533 }
534
535 fn write_minify(
536     fs: &DocFS,
537     dst: PathBuf,
538     contents: &str,
539     enable_minification: bool,
540 ) -> Result<(), Error> {
541     if enable_minification {
542         if dst.extension() == Some(&OsStr::new("css")) {
543             let res = try_none!(minifier::css::minify(contents).ok(), &dst);
544             fs.write(dst, res.as_bytes())
545         } else {
546             fs.write(dst, minifier::js::minify(contents).as_bytes())
547         }
548     } else {
549         fs.write(dst, contents.as_bytes())
550     }
551 }