]> git.lizzy.rs Git - rust.git/blob - src/librustdoc/html/render/write_shared.rs
Rollup merge of #81917 - rust-lang:relnotes-1.51.0, r=Mark-Simulacrum
[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.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,
40     }
41 });
42
43 pub(super) fn write_shared(
44     cx: &Context<'_>,
45     krate: &Crate,
46     search_index: String,
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);
54
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.
57
58     write_minify(
59         &cx.shared.fs,
60         cx.path("rustdoc.css"),
61         static_files::RUSTDOC_CSS,
62         options.enable_minification,
63     )?;
64     write_minify(
65         &cx.shared.fs,
66         cx.path("settings.css"),
67         static_files::SETTINGS_CSS,
68         options.enable_minification,
69     )?;
70     write_minify(
71         &cx.shared.fs,
72         cx.path("noscript.css"),
73         static_files::NOSCRIPT_CSS,
74         options.enable_minification,
75     )?;
76
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();
80
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);
83         let extension =
84             try_none!(try_none!(entry.path.extension(), &entry.path).to_str(), &entry.path);
85
86         // Handle the official themes
87         match theme {
88             "light" => write_minify(
89                 &cx.shared.fs,
90                 cx.path("light.css"),
91                 static_files::themes::LIGHT,
92                 options.enable_minification,
93             )?,
94             "dark" => write_minify(
95                 &cx.shared.fs,
96                 cx.path("dark.css"),
97                 static_files::themes::DARK,
98                 options.enable_minification,
99             )?,
100             "ayu" => write_minify(
101                 &cx.shared.fs,
102                 cx.path("ayu.css"),
103                 static_files::themes::AYU,
104                 options.enable_minification,
105             )?,
106             _ => {
107                 // Handle added third-party themes
108                 let content = try_err!(fs::read(&entry.path), &entry.path);
109                 cx.shared
110                     .fs
111                     .write(cx.path(&format!("{}.{}", theme, extension)), content.as_slice())?;
112             }
113         };
114
115         themes.insert(theme.to_owned());
116     }
117
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)?;
121     }
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)?;
126     }
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)?;
130
131     let mut themes: Vec<&String> = themes.iter().collect();
132     themes.sort();
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");
138
139 function showThemeButtonState() {{
140     themes.style.display = "block";
141     themePicker.style.borderBottomRightRadius = "0";
142     themePicker.style.borderBottomLeftRadius = "0";
143 }}
144
145 function hideThemeButtonState() {{
146     themes.style.display = "none";
147     themePicker.style.borderBottomRightRadius = "3px";
148     themePicker.style.borderBottomLeftRadius = "3px";
149 }}
150
151 function switchThemeButtonState() {{
152     if (themes.style.display === "block") {{
153         hideThemeButtonState();
154     }} else {{
155         showThemeButtonState();
156     }}
157 }};
158
159 function handleThemeButtonsBlur(e) {{
160     var active = document.activeElement;
161     var related = e.relatedTarget;
162
163     if (active.id !== "theme-picker" &&
164         (!active.parentNode || active.parentNode.id !== "theme-choices") &&
165         (!related ||
166          (related.id !== "theme-picker" &&
167           (!related.parentNode || related.parentNode.id !== "theme-choices")))) {{
168         hideThemeButtonState();
169     }}
170 }}
171
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);
180     }};
181     but.onblur = handleThemeButtonsBlur;
182     themes.appendChild(but);
183 }});"#,
184         serde_json::to_string(&themes).unwrap()
185     );
186
187     write_minify(&cx.shared.fs, cx.path("theme.js"), &theme_js, options.enable_minification)?;
188     write_minify(
189         &cx.shared.fs,
190         cx.path("main.js"),
191         static_files::MAIN_JS,
192         options.enable_minification,
193     )?;
194     write_minify(
195         &cx.shared.fs,
196         cx.path("settings.js"),
197         static_files::SETTINGS_JS,
198         options.enable_minification,
199     )?;
200     if cx.shared.include_sources {
201         write_minify(
202             &cx.shared.fs,
203             cx.path("source-script.js"),
204             static_files::sidebar::SOURCE_SCRIPT,
205             options.enable_minification,
206         )?;
207     }
208
209     {
210         write_minify(
211             &cx.shared.fs,
212             cx.path("storage.js"),
213             &format!(
214                 "var resourcesSuffix = \"{}\";{}",
215                 cx.shared.resource_suffix,
216                 static_files::STORAGE_JS
217             ),
218             options.enable_minification,
219         )?;
220     }
221
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)?;
227         } else {
228             write_minify(&cx.shared.fs, out, &buffer, options.enable_minification)?;
229         }
230     }
231     write_minify(
232         &cx.shared.fs,
233         cx.path("normalize.css"),
234         static_files::NORMALIZE_CSS,
235         options.enable_minification,
236     )?;
237     for (file, contents) in &*FILES_UNVERSIONED {
238         write(cx.dst.join(file), contents)?;
239     }
240
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();
244
245         if path.exists() {
246             let prefix = format!(r#"{}["{}"]"#, key, krate);
247             for line in BufReader::new(File::open(path)?).lines() {
248                 let line = line?;
249                 if !line.starts_with(key) {
250                     continue;
251                 }
252                 if line.starts_with(&prefix) {
253                     continue;
254                 }
255                 ret.push(line.to_string());
256                 krates.push(
257                     line[key.len() + 2..]
258                         .split('"')
259                         .next()
260                         .map(|s| s.to_owned())
261                         .unwrap_or_else(String::new),
262                 );
263             }
264         }
265         Ok((ret, krates))
266     }
267
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();
271
272         if path.exists() {
273             let prefix = format!("\"{}\"", krate);
274             for line in BufReader::new(File::open(path)?).lines() {
275                 let line = line?;
276                 if !line.starts_with('"') {
277                     continue;
278                 }
279                 if line.starts_with(&prefix) {
280                     continue;
281                 }
282                 if line.ends_with(",\\") {
283                     ret.push(line[..line.len() - 2].to_string());
284                 } else {
285                     // Ends with "\\" (it's the case for the last added crate line)
286                     ret.push(line[..line.len() - 1].to_string());
287                 }
288                 krates.push(
289                     line.split('"')
290                         .find(|s| !s.is_empty())
291                         .map(|s| s.to_owned())
292                         .unwrap_or_else(String::new),
293                 );
294             }
295         }
296         Ok((ret, krates))
297     }
298
299     use std::ffi::OsString;
300
301     #[derive(Debug)]
302     struct Hierarchy {
303         elem: OsString,
304         children: FxHashMap<OsString, Hierarchy>,
305         elems: FxHashSet<OsString>,
306     }
307
308     impl Hierarchy {
309         fn new(elem: OsString) -> Hierarchy {
310             Hierarchy { elem, children: FxHashMap::default(), elems: FxHashSet::default() }
311         }
312
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));
316             let mut files = self
317                 .elems
318                 .iter()
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(",");
323             let dirs =
324                 if subs.is_empty() { String::new() } else { format!(",\"dirs\":[{}]", subs) };
325             let files = files.join(",");
326             let files =
327                 if files.is_empty() { String::new() } else { format!(",\"files\":[{}]", files) };
328             format!(
329                 "{{\"name\":\"{name}\"{dirs}{files}}}",
330                 name = self.elem.to_str().expect("invalid osstring conversion"),
331                 dirs = dirs,
332                 files = files
333             )
334         }
335     }
336
337     if cx.shared.include_sources {
338         let mut hierarchy = Hierarchy::new(OsString::new());
339         for source in cx
340             .shared
341             .local_sources
342             .iter()
343             .filter_map(|p| p.0.strip_prefix(&cx.shared.src_root).ok())
344         {
345             let mut h = &mut hierarchy;
346             let mut elems = source
347                 .components()
348                 .filter_map(|s| match s {
349                     Component::Normal(s) => Some(s.to_owned()),
350                     _ => None,
351                 })
352                 .peekable();
353             loop {
354                 let cur_elem = elems.next().expect("empty file path");
355                 if elems.peek().is_none() {
356                     h.elems.insert(cur_elem);
357                     break;
358                 } else {
359                     let e = cur_elem.clone();
360                     h = h.children.entry(cur_elem.clone()).or_insert_with(|| Hierarchy::new(e));
361                 }
362             }
363         }
364
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[\"{}\"] = {};",
370             &krate.name,
371             hierarchy.to_json_string()
372         ));
373         all_sources.sort();
374         let v = format!(
375             "var N = null;var sourcesIndex = {{}};\n{}\ncreateSourceSidebar();\n",
376             all_sources.join("\n")
377         );
378         cx.shared.fs.write(&dst, v.as_bytes())?;
379     }
380
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());
386     krates.sort();
387
388     // Sort the indexes by crate so the file will be generated identically even
389     // with rustdoc running in parallel.
390     all_indexes.sort();
391     {
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)?;
396     }
397
398     let crate_list_dst = cx.dst.join(&format!("crates{}.js", cx.shared.resource_suffix));
399     let crate_list =
400         format!("window.ALL_CRATES = [{}];", krates.iter().map(|k| format!("\"{}\"", k)).join(","));
401     cx.shared.fs.write(&crate_list_dst, &crate_list)?;
402
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();
408
409             crate::markdown::render(&index_page, md_opts, cx.shared.edition)
410                 .map_err(|e| Error::new(e, &index_page))?;
411         } else {
412             let dst = cx.dst.join("index.html");
413             let page = layout::Page {
414                 title: "Index of crates",
415                 css_class: "mod",
416                 root_path: "./",
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,
421                 extra_scripts: &[],
422                 static_extra_scripts: &[],
423             };
424
425             let content = format!(
426                 "<h1 class=\"fqn\">\
427                      <span class=\"in-band\">List of all crates</span>\
428                 </h1><ul class=\"crate mod\">{}</ul>",
429                 krates
430                     .iter()
431                     .map(|s| {
432                         format!(
433                             "<li><a class=\"crate mod\" href=\"{}index.html\">{}</a></li>",
434                             ensure_trailing_slash(s),
435                             s
436                         )
437                     })
438                     .collect::<String>()
439             );
440             let v = layout::render(&cx.shared.layout, &page, "", content, &cx.shared.style_files);
441             cx.shared.fs.write(&dst, v.as_bytes())?;
442         }
443     }
444
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.
452         //
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) {
456             Some(p) => p,
457             None => match cx.cache.external_paths.get(&did) {
458                 Some(p) => p,
459                 None => continue,
460             },
461         };
462
463         #[derive(Serialize)]
464         struct Implementor {
465             text: String,
466             synthetic: bool,
467             types: Vec<String>,
468         }
469
470         let implementors = imps
471             .iter()
472             .filter_map(|imp| {
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.
477                 //
478                 // If the implementation is from another crate then that crate
479                 // should add it.
480                 if imp.impl_item.def_id.krate == did.krate || !imp.impl_item.def_id.is_local() {
481                     None
482                 } else {
483                     Some(Implementor {
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()),
487                     })
488                 }
489             })
490             .collect::<Vec<_>>();
491
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
494         // links.
495         if implementors.is_empty() && !cx.cache.paths.contains_key(&did) {
496             continue;
497         }
498
499         let implementors = format!(
500             r#"implementors["{}"] = {};"#,
501             krate.name,
502             serde_json::to_string(&implementors).unwrap()
503         );
504
505         let mut mydst = dst.clone();
506         for part in &remote_path[..remote_path.len() - 1] {
507             mydst.push(part);
508         }
509         cx.shared.ensure_dir(&mydst)?;
510         mydst.push(&format!("{}.{}.js", remote_item_type, remote_path[remote_path.len() - 1]));
511
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();
518
519         let mut v = String::from("(function() {var implementors = {};\n");
520         for implementor in &all_implementors {
521             writeln!(v, "{}", *implementor).unwrap();
522         }
523         v.push_str(
524             "if (window.register_implementors) {\
525                  window.register_implementors(implementors);\
526              } else {\
527                  window.pending_implementors = implementors;\
528              }",
529         );
530         v.push_str("})()");
531         cx.shared.fs.write(&mydst, &v)?;
532     }
533     Ok(())
534 }
535
536 fn write_minify(
537     fs: &DocFS,
538     dst: PathBuf,
539     contents: &str,
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())
546         } else {
547             fs.write(dst, minifier::js::minify(contents).as_bytes())
548         }
549     } else {
550         fs.write(dst, contents.as_bytes())
551     }
552 }