]> git.lizzy.rs Git - rust.git/blob - src/librustdoc/html/render/write_shared.rs
Added docs to internal_macro const
[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::{EmitType, RenderOptions};
17 use crate::docfs::PathError;
18 use crate::error::Error;
19 use crate::html::{layout, static_files};
20
21 static FILES_UNVERSIONED: Lazy<FxHashMap<&str, &[u8]>> = Lazy::new(|| {
22     map! {
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,
48     }
49 });
50
51 enum SharedResource<'a> {
52     /// This file will never change, no matter what toolchain is used to build it.
53     ///
54     /// It does not have a resource suffix.
55     Unversioned { name: &'static str },
56     /// This file may change depending on the toolchain.
57     ///
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.
61     ///
62     /// This differs from normal invocation-specific files because it has a resource suffix.
63     InvocationSpecific { basename: &'a str },
64 }
65
66 impl SharedResource<'_> {
67     fn extension(&self) -> Option<&OsStr> {
68         use SharedResource::*;
69         match self {
70             Unversioned { name }
71             | ToolchainSpecific { basename: name }
72             | InvocationSpecific { basename: name } => Path::new(name).extension(),
73         }
74     }
75
76     fn path(&self, cx: &Context<'_>) -> PathBuf {
77         match self {
78             SharedResource::Unversioned { name } => cx.dst.join(name),
79             SharedResource::ToolchainSpecific { basename } => cx.suffix_path(basename),
80             SharedResource::InvocationSpecific { basename } => cx.suffix_path(basename),
81         }
82     }
83
84     fn should_emit(&self, emit: &[EmitType]) -> bool {
85         if emit.is_empty() {
86             return true;
87         }
88         let kind = match self {
89             SharedResource::Unversioned { .. } => EmitType::Unversioned,
90             SharedResource::ToolchainSpecific { .. } => EmitType::Toolchain,
91             SharedResource::InvocationSpecific { .. } => EmitType::InvocationSpecific,
92         };
93         emit.contains(&kind)
94     }
95 }
96
97 impl Context<'_> {
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
103         // want.
104         let (base, ext) = filename.split_once('.').unwrap();
105         let filename = format!("{}{}.{}", base, self.shared.resource_suffix, ext);
106         self.dst.join(&filename)
107     }
108
109     fn write_shared(
110         &self,
111         resource: SharedResource<'_>,
112         contents: impl 'static + Send + AsRef<[u8]>,
113         emit: &[EmitType],
114     ) -> Result<(), Error> {
115         if resource.should_emit(emit) {
116             self.shared.fs.write(resource.path(self), contents)
117         } else {
118             Ok(())
119         }
120     }
121
122     fn write_minify(
123         &self,
124         resource: SharedResource<'_>,
125         contents: impl 'static + Send + AsRef<str> + AsRef<[u8]>,
126         minify: bool,
127         emit: &[EmitType],
128     ) -> Result<(), Error> {
129         if minify {
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))
134                 })?
135             } else {
136                 minifier::js::minify(contents)
137             };
138             self.write_shared(resource, contents, emit)
139         } else {
140             self.write_shared(resource, contents, emit)
141         }
142     }
143 }
144
145 pub(super) fn write_shared(
146     cx: &Context<'_>,
147     krate: &Crate,
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);
156
157     // Minified resources are usually toolchain resources. If they're not, they should use `cx.write_minify` directly.
158     fn write_minify(
159         basename: &'static str,
160         contents: impl 'static + Send + AsRef<str> + AsRef<[u8]>,
161         cx: &Context<'_>,
162         options: &RenderOptions,
163     ) -> Result<(), Error> {
164         cx.write_minify(
165             SharedResource::ToolchainSpecific { basename },
166             contents,
167             options.enable_minification,
168             &options.emit,
169         )
170     }
171
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)
175     };
176
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)
181     };
182
183     fn add_background_image_to_css(
184         cx: &Context<'_>,
185         css: &mut String,
186         rule: &str,
187         file: &'static str,
188     ) {
189         css.push_str(&format!(
190             "{} {{ background-image: url({}); }}",
191             rule,
192             SharedResource::ToolchainSpecific { basename: file }
193                 .path(cx)
194                 .file_name()
195                 .unwrap()
196                 .to_str()
197                 .unwrap()
198         ))
199     }
200
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(
205         cx,
206         &mut rustdoc_css,
207         "details.undocumented[open] > summary::before, \
208          details.rustdoc-toggle[open] > summary::before, \
209          details.rustdoc-toggle[open] > summary.hideme::before",
210         "toggle-minus.svg",
211     );
212     add_background_image_to_css(
213         cx,
214         &mut rustdoc_css,
215         "details.undocumented > summary::before, details.rustdoc-toggle > summary::before",
216         "toggle-plus.svg",
217     );
218     write_minify("rustdoc.css", rustdoc_css, cx, options)?;
219
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)?;
224
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();
228
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);
231         let extension =
232             try_none!(try_none!(entry.path.extension(), &entry.path).to_str(), &entry.path);
233
234         // Handle the official themes
235         match theme {
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)?,
239             _ => {
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)))?;
243             }
244         };
245
246         themes.insert(theme.to_owned());
247     }
248
249     if (*cx.shared).layout.logo.is_empty() {
250         write_toolchain("rust-logo.png", static_files::RUST_LOGO)?;
251     }
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)?;
256     }
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)?;
263
264     let mut themes: Vec<&String> = themes.iter().collect();
265     themes.sort();
266
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?
270     write_minify(
271         "main.js",
272         static_files::MAIN_JS
273             .replace(
274                 "/* INSERT THEMES HERE */",
275                 &format!(" = {}", serde_json::to_string(&themes).unwrap()),
276             )
277             .replace(
278                 "/* INSERT RUSTDOC_VERSION HERE */",
279                 &format!(
280                     "rustdoc {}",
281                     rustc_interface::util::version_str().unwrap_or("unknown version")
282                 ),
283             ),
284         cx,
285         options,
286     )?;
287     write_minify("search.js", static_files::SEARCH_JS, cx, options)?;
288     write_minify("settings.js", static_files::SETTINGS_JS, cx, options)?;
289
290     if cx.include_sources {
291         write_minify("source-script.js", static_files::sidebar::SOURCE_SCRIPT, cx, options)?;
292     }
293
294     {
295         write_minify(
296             "storage.js",
297             format!(
298                 "var resourcesSuffix = \"{}\";{}",
299                 cx.shared.resource_suffix,
300                 static_files::STORAGE_JS
301             ),
302             cx,
303             options,
304         )?;
305     }
306
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.
310         cx.write_minify(
311             SharedResource::InvocationSpecific { basename: "theme.css" },
312             buffer,
313             options.enable_minification,
314             &options.emit,
315         )?;
316     }
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)?;
320     }
321
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();
325
326         if path.exists() {
327             let prefix = format!(r#"{}["{}"]"#, key, krate);
328             for line in BufReader::new(File::open(path)?).lines() {
329                 let line = line?;
330                 if !line.starts_with(key) {
331                     continue;
332                 }
333                 if line.starts_with(&prefix) {
334                     continue;
335                 }
336                 ret.push(line.to_string());
337                 krates.push(
338                     line[key.len() + 2..]
339                         .split('"')
340                         .next()
341                         .map(|s| s.to_owned())
342                         .unwrap_or_else(String::new),
343                 );
344             }
345         }
346         Ok((ret, krates))
347     }
348
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();
352
353         if path.exists() {
354             let prefix = format!("\"{}\"", krate);
355             for line in BufReader::new(File::open(path)?).lines() {
356                 let line = line?;
357                 if !line.starts_with('"') {
358                     continue;
359                 }
360                 if line.starts_with(&prefix) {
361                     continue;
362                 }
363                 if line.ends_with(",\\") {
364                     ret.push(line[..line.len() - 2].to_string());
365                 } else {
366                     // Ends with "\\" (it's the case for the last added crate line)
367                     ret.push(line[..line.len() - 1].to_string());
368                 }
369                 krates.push(
370                     line.split('"')
371                         .find(|s| !s.is_empty())
372                         .map(|s| s.to_owned())
373                         .unwrap_or_else(String::new),
374                 );
375             }
376         }
377         Ok((ret, krates))
378     }
379
380     use std::ffi::OsString;
381
382     #[derive(Debug)]
383     struct Hierarchy {
384         elem: OsString,
385         children: FxHashMap<OsString, Hierarchy>,
386         elems: FxHashSet<OsString>,
387     }
388
389     impl Hierarchy {
390         fn new(elem: OsString) -> Hierarchy {
391             Hierarchy { elem, children: FxHashMap::default(), elems: FxHashSet::default() }
392         }
393
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));
397             let mut files = self
398                 .elems
399                 .iter()
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(",");
404             let dirs =
405                 if subs.is_empty() { String::new() } else { format!(",\"dirs\":[{}]", subs) };
406             let files = files.join(",");
407             let files =
408                 if files.is_empty() { String::new() } else { format!(",\"files\":[{}]", files) };
409             format!(
410                 "{{\"name\":\"{name}\"{dirs}{files}}}",
411                 name = self.elem.to_str().expect("invalid osstring conversion"),
412                 dirs = dirs,
413                 files = files
414             )
415         }
416     }
417
418     if cx.include_sources {
419         let mut hierarchy = Hierarchy::new(OsString::new());
420         for source in cx
421             .shared
422             .local_sources
423             .iter()
424             .filter_map(|p| p.0.strip_prefix(&cx.shared.src_root).ok())
425         {
426             let mut h = &mut hierarchy;
427             let mut elems = source
428                 .components()
429                 .filter_map(|s| match s {
430                     Component::Normal(s) => Some(s.to_owned()),
431                     _ => None,
432                 })
433                 .peekable();
434             loop {
435                 let cur_elem = elems.next().expect("empty file path");
436                 if elems.peek().is_none() {
437                     h.elems.insert(cur_elem);
438                     break;
439                 } else {
440                     let e = cur_elem.clone();
441                     h = h.children.entry(cur_elem.clone()).or_insert_with(|| Hierarchy::new(e));
442                 }
443             }
444         }
445
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[\"{}\"] = {};",
452                 &krate.name,
453                 hierarchy.to_json_string()
454             ));
455             all_sources.sort();
456             Ok(format!(
457                 "var N = null;var sourcesIndex = {{}};\n{}\ncreateSourceSidebar();\n",
458                 all_sources.join("\n")
459             )
460             .into_bytes())
461         };
462         write_crate("source-files.js", &make_sources)?;
463     }
464
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());
470     krates.sort();
471
472     // Sort the indexes by crate so the file will be generated identically even
473     // with rustdoc running in parallel.
474     all_indexes.sort();
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)};");
479         Ok(v.into_bytes())
480     })?;
481
482     write_crate("crates.js", &|| {
483         let krates = krates.iter().map(|k| format!("\"{}\"", k)).join(",");
484         Ok(format!("window.ALL_CRATES = [{}];", krates).into_bytes())
485     })?;
486
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();
492
493             crate::markdown::render(&index_page, md_opts, cx.shared.edition())
494                 .map_err(|e| Error::new(e, &index_page))?;
495         } else {
496             let dst = cx.dst.join("index.html");
497             let page = layout::Page {
498                 title: "Index of crates",
499                 css_class: "mod",
500                 root_path: "./",
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,
505                 extra_scripts: &[],
506                 static_extra_scripts: &[],
507             };
508
509             let content = format!(
510                 "<h1 class=\"fqn\">\
511                      <span class=\"in-band\">List of all crates</span>\
512                 </h1><ul class=\"crate mod\">{}</ul>",
513                 krates
514                     .iter()
515                     .map(|s| {
516                         format!(
517                             "<li><a class=\"crate mod\" href=\"{}index.html\">{}</a></li>",
518                             ensure_trailing_slash(s),
519                             s
520                         )
521                     })
522                     .collect::<String>()
523             );
524             let v = layout::render(
525                 &cx.shared.templates,
526                 &cx.shared.layout,
527                 &page,
528                 "",
529                 content,
530                 &cx.shared.style_files,
531             );
532             cx.shared.fs.write(dst, v)?;
533         }
534     }
535
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.
544         //
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) {
548             Some(p) => p,
549             None => match cache.external_paths.get(&did) {
550                 Some(p) => p,
551                 None => continue,
552             },
553         };
554
555         #[derive(Serialize)]
556         struct Implementor {
557             text: String,
558             synthetic: bool,
559             types: Vec<String>,
560         }
561
562         let implementors = imps
563             .iter()
564             .filter_map(|imp| {
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.
569                 //
570                 // If the implementation is from another crate then that crate
571                 // should add it.
572                 if imp.impl_item.def_id.krate() == did.krate || !imp.impl_item.def_id.is_local() {
573                     None
574                 } else {
575                     Some(Implementor {
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),
579                     })
580                 }
581             })
582             .collect::<Vec<_>>();
583
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
586         // links.
587         if implementors.is_empty() && !cache.paths.contains_key(&did) {
588             continue;
589         }
590
591         let implementors = format!(
592             r#"implementors["{}"] = {};"#,
593             krate.name,
594             serde_json::to_string(&implementors).unwrap()
595         );
596
597         let mut mydst = dst.clone();
598         for part in &remote_path[..remote_path.len() - 1] {
599             mydst.push(part);
600         }
601         cx.shared.ensure_dir(&mydst)?;
602         mydst.push(&format!("{}.{}.js", remote_item_type, remote_path[remote_path.len() - 1]));
603
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();
610
611         let mut v = String::from("(function() {var implementors = {};\n");
612         for implementor in &all_implementors {
613             writeln!(v, "{}", *implementor).unwrap();
614         }
615         v.push_str(
616             "if (window.register_implementors) {\
617                  window.register_implementors(implementors);\
618              } else {\
619                  window.pending_implementors = implementors;\
620              }",
621         );
622         v.push_str("})()");
623         cx.shared.fs.write(mydst, v)?;
624     }
625     Ok(())
626 }