]> git.lizzy.rs Git - rust.git/blob - src/librustdoc/html/render/write_shared.rs
Auto merge of #107000 - GuillaumeGomez:fix-items-in-doc-hidden-block, r=notriddle...
[rust.git] / src / librustdoc / html / render / write_shared.rs
1 use std::cell::RefCell;
2 use std::fs::{self, File};
3 use std::io::prelude::*;
4 use std::io::{self, BufReader};
5 use std::path::{Component, Path};
6 use std::rc::{Rc, Weak};
7
8 use itertools::Itertools;
9 use rustc_data_structures::flock;
10 use rustc_data_structures::fx::{FxHashMap, FxHashSet};
11 use serde::ser::SerializeSeq;
12 use serde::{Serialize, Serializer};
13
14 use super::{collect_paths_for_type, ensure_trailing_slash, Context};
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 use crate::{try_err, try_none};
21
22 /// Rustdoc writes out two kinds of shared files:
23 ///  - Static files, which are embedded in the rustdoc binary and are written with a
24 ///    filename that includes a hash of their contents. These will always have a new
25 ///    URL if the contents change, so they are safe to cache with the
26 ///    `Cache-Control: immutable` directive. They are written under the static.files/
27 ///    directory and are written when --emit-type is empty (default) or contains
28 ///    "toolchain-specific". If using the --static-root-path flag, it should point
29 ///    to a URL path prefix where each of these filenames can be fetched.
30 ///  - Invocation specific files. These are generated based on the crate(s) being
31 ///    documented. Their filenames need to be predictable without knowing their
32 ///    contents, so they do not include a hash in their filename and are not safe to
33 ///    cache with `Cache-Control: immutable`. They include the contents of the
34 ///    --resource-suffix flag and are emitted when --emit-type is empty (default)
35 ///    or contains "invocation-specific".
36 pub(super) fn write_shared(
37     cx: &mut Context<'_>,
38     krate: &Crate,
39     search_index: String,
40     options: &RenderOptions,
41 ) -> Result<(), Error> {
42     // Write out the shared files. Note that these are shared among all rustdoc
43     // docs placed in the output directory, so this needs to be a synchronized
44     // operation with respect to all other rustdocs running around.
45     let lock_file = cx.dst.join(".lock");
46     let _lock = try_err!(flock::Lock::new(&lock_file, true, true, true), &lock_file);
47
48     // InvocationSpecific resources should always be dynamic.
49     let write_invocation_specific = |p: &str, make_content: &dyn Fn() -> Result<Vec<u8>, Error>| {
50         let content = make_content()?;
51         if options.emit.is_empty() || options.emit.contains(&EmitType::InvocationSpecific) {
52             let output_filename = static_files::suffix_path(p, &cx.shared.resource_suffix);
53             cx.shared.fs.write(cx.dst.join(output_filename), content)
54         } else {
55             Ok(())
56         }
57     };
58
59     cx.shared
60         .fs
61         .create_dir_all(cx.dst.join("static.files"))
62         .map_err(|e| PathError::new(e, "static.files"))?;
63
64     // Handle added third-party themes
65     for entry in &cx.shared.style_files {
66         let theme = entry.basename()?;
67         let extension =
68             try_none!(try_none!(entry.path.extension(), &entry.path).to_str(), &entry.path);
69
70         // Skip the official themes. They are written below as part of STATIC_FILES_LIST.
71         if matches!(theme.as_str(), "light" | "dark" | "ayu") {
72             continue;
73         }
74
75         let bytes = try_err!(fs::read(&entry.path), &entry.path);
76         let filename = format!("{}{}.{}", theme, cx.shared.resource_suffix, extension);
77         cx.shared.fs.write(cx.dst.join(filename), bytes)?;
78     }
79
80     // When the user adds their own CSS files with --extend-css, we write that as an
81     // invocation-specific file (that is, with a resource suffix).
82     if let Some(ref css) = cx.shared.layout.css_file_extension {
83         let buffer = try_err!(fs::read_to_string(css), css);
84         let path = static_files::suffix_path("theme.css", &cx.shared.resource_suffix);
85         cx.shared.fs.write(cx.dst.join(path), buffer)?;
86     }
87
88     if options.emit.is_empty() || options.emit.contains(&EmitType::Toolchain) {
89         let static_dir = cx.dst.join(Path::new("static.files"));
90         static_files::for_each(|f: &static_files::StaticFile| {
91             let filename = static_dir.join(f.output_filename());
92             cx.shared.fs.write(filename, f.minified())
93         })?;
94     }
95
96     /// Read a file and return all lines that match the `"{crate}":{data},` format,
97     /// and return a tuple `(Vec<DataString>, Vec<CrateNameString>)`.
98     ///
99     /// This forms the payload of files that look like this:
100     ///
101     /// ```javascript
102     /// var data = {
103     /// "{crate1}":{data},
104     /// "{crate2}":{data}
105     /// };
106     /// use_data(data);
107     /// ```
108     ///
109     /// The file needs to be formatted so that *only crate data lines start with `"`*.
110     fn collect(path: &Path, krate: &str) -> io::Result<(Vec<String>, Vec<String>)> {
111         let mut ret = Vec::new();
112         let mut krates = Vec::new();
113
114         if path.exists() {
115             let prefix = format!("\"{}\"", krate);
116             for line in BufReader::new(File::open(path)?).lines() {
117                 let line = line?;
118                 if !line.starts_with('"') {
119                     continue;
120                 }
121                 if line.starts_with(&prefix) {
122                     continue;
123                 }
124                 if line.ends_with(',') {
125                     ret.push(line[..line.len() - 1].to_string());
126                 } else {
127                     // No comma (it's the case for the last added crate line)
128                     ret.push(line.to_string());
129                 }
130                 krates.push(
131                     line.split('"')
132                         .find(|s| !s.is_empty())
133                         .map(|s| s.to_owned())
134                         .unwrap_or_else(String::new),
135                 );
136             }
137         }
138         Ok((ret, krates))
139     }
140
141     /// Read a file and return all lines that match the <code>"{crate}":{data},\ </code> format,
142     /// and return a tuple `(Vec<DataString>, Vec<CrateNameString>)`.
143     ///
144     /// This forms the payload of files that look like this:
145     ///
146     /// ```javascript
147     /// var data = JSON.parse('{\
148     /// "{crate1}":{data},\
149     /// "{crate2}":{data}\
150     /// }');
151     /// use_data(data);
152     /// ```
153     ///
154     /// The file needs to be formatted so that *only crate data lines start with `"`*.
155     fn collect_json(path: &Path, krate: &str) -> io::Result<(Vec<String>, Vec<String>)> {
156         let mut ret = Vec::new();
157         let mut krates = Vec::new();
158
159         if path.exists() {
160             let prefix = format!("\"{}\"", krate);
161             for line in BufReader::new(File::open(path)?).lines() {
162                 let line = line?;
163                 if !line.starts_with('"') {
164                     continue;
165                 }
166                 if line.starts_with(&prefix) {
167                     continue;
168                 }
169                 if line.ends_with(",\\") {
170                     ret.push(line[..line.len() - 2].to_string());
171                 } else {
172                     // Ends with "\\" (it's the case for the last added crate line)
173                     ret.push(line[..line.len() - 1].to_string());
174                 }
175                 krates.push(
176                     line.split('"')
177                         .find(|s| !s.is_empty())
178                         .map(|s| s.to_owned())
179                         .unwrap_or_else(String::new),
180                 );
181             }
182         }
183         Ok((ret, krates))
184     }
185
186     use std::ffi::OsString;
187
188     #[derive(Debug, Default)]
189     struct Hierarchy {
190         parent: Weak<Self>,
191         elem: OsString,
192         children: RefCell<FxHashMap<OsString, Rc<Self>>>,
193         elems: RefCell<FxHashSet<OsString>>,
194     }
195
196     impl Hierarchy {
197         fn with_parent(elem: OsString, parent: &Rc<Self>) -> Self {
198             Self { elem, parent: Rc::downgrade(parent), ..Self::default() }
199         }
200
201         fn to_json_string(&self) -> String {
202             let borrow = self.children.borrow();
203             let mut subs: Vec<_> = borrow.values().collect();
204             subs.sort_unstable_by(|a, b| a.elem.cmp(&b.elem));
205             let mut files = self
206                 .elems
207                 .borrow()
208                 .iter()
209                 .map(|s| format!("\"{}\"", s.to_str().expect("invalid osstring conversion")))
210                 .collect::<Vec<_>>();
211             files.sort_unstable();
212             let subs = subs.iter().map(|s| s.to_json_string()).collect::<Vec<_>>().join(",");
213             let dirs = if subs.is_empty() && files.is_empty() {
214                 String::new()
215             } else {
216                 format!(",[{}]", subs)
217             };
218             let files = files.join(",");
219             let files = if files.is_empty() { String::new() } else { format!(",[{}]", files) };
220             format!(
221                 "[\"{name}\"{dirs}{files}]",
222                 name = self.elem.to_str().expect("invalid osstring conversion"),
223                 dirs = dirs,
224                 files = files
225             )
226         }
227
228         fn add_path(self: &Rc<Self>, path: &Path) {
229             let mut h = Rc::clone(&self);
230             let mut elems = path
231                 .components()
232                 .filter_map(|s| match s {
233                     Component::Normal(s) => Some(s.to_owned()),
234                     Component::ParentDir => Some(OsString::from("..")),
235                     _ => None,
236                 })
237                 .peekable();
238             loop {
239                 let cur_elem = elems.next().expect("empty file path");
240                 if cur_elem == ".." {
241                     if let Some(parent) = h.parent.upgrade() {
242                         h = parent;
243                     }
244                     continue;
245                 }
246                 if elems.peek().is_none() {
247                     h.elems.borrow_mut().insert(cur_elem);
248                     break;
249                 } else {
250                     let entry = Rc::clone(
251                         h.children
252                             .borrow_mut()
253                             .entry(cur_elem.clone())
254                             .or_insert_with(|| Rc::new(Self::with_parent(cur_elem, &h))),
255                     );
256                     h = entry;
257                 }
258             }
259         }
260     }
261
262     if cx.include_sources {
263         let hierarchy = Rc::new(Hierarchy::default());
264         for source in cx
265             .shared
266             .local_sources
267             .iter()
268             .filter_map(|p| p.0.strip_prefix(&cx.shared.src_root).ok())
269         {
270             hierarchy.add_path(source);
271         }
272         let hierarchy = Rc::try_unwrap(hierarchy).unwrap();
273         let dst = cx.dst.join(&format!("source-files{}.js", cx.shared.resource_suffix));
274         let make_sources = || {
275             let (mut all_sources, _krates) =
276                 try_err!(collect_json(&dst, krate.name(cx.tcx()).as_str()), &dst);
277             all_sources.push(format!(
278                 r#""{}":{}"#,
279                 &krate.name(cx.tcx()),
280                 hierarchy
281                     .to_json_string()
282                     // All these `replace` calls are because we have to go through JS string for JSON content.
283                     .replace('\\', r"\\")
284                     .replace('\'', r"\'")
285                     // We need to escape double quotes for the JSON.
286                     .replace("\\\"", "\\\\\"")
287             ));
288             all_sources.sort();
289             let mut v = String::from("var sourcesIndex = JSON.parse('{\\\n");
290             v.push_str(&all_sources.join(",\\\n"));
291             v.push_str("\\\n}');\ncreateSourceSidebar();\n");
292             Ok(v.into_bytes())
293         };
294         write_invocation_specific("source-files.js", &make_sources)?;
295     }
296
297     // Update the search index and crate list.
298     let dst = cx.dst.join(&format!("search-index{}.js", cx.shared.resource_suffix));
299     let (mut all_indexes, mut krates) =
300         try_err!(collect_json(&dst, krate.name(cx.tcx()).as_str()), &dst);
301     all_indexes.push(search_index);
302     krates.push(krate.name(cx.tcx()).to_string());
303     krates.sort();
304
305     // Sort the indexes by crate so the file will be generated identically even
306     // with rustdoc running in parallel.
307     all_indexes.sort();
308     write_invocation_specific("search-index.js", &|| {
309         let mut v = String::from("var searchIndex = JSON.parse('{\\\n");
310         v.push_str(&all_indexes.join(",\\\n"));
311         v.push_str(
312             r#"\
313 }');
314 if (typeof window !== 'undefined' && window.initSearch) {window.initSearch(searchIndex)};
315 if (typeof exports !== 'undefined') {exports.searchIndex = searchIndex};
316 "#,
317         );
318         Ok(v.into_bytes())
319     })?;
320
321     write_invocation_specific("crates.js", &|| {
322         let krates = krates.iter().map(|k| format!("\"{}\"", k)).join(",");
323         Ok(format!("window.ALL_CRATES = [{}];", krates).into_bytes())
324     })?;
325
326     if options.enable_index_page {
327         if let Some(index_page) = options.index_page.clone() {
328             let mut md_opts = options.clone();
329             md_opts.output = cx.dst.clone();
330             md_opts.external_html = (*cx.shared).layout.external_html.clone();
331
332             crate::markdown::render(&index_page, md_opts, cx.shared.edition())
333                 .map_err(|e| Error::new(e, &index_page))?;
334         } else {
335             let shared = Rc::clone(&cx.shared);
336             let dst = cx.dst.join("index.html");
337             let page = layout::Page {
338                 title: "Index of crates",
339                 css_class: "mod",
340                 root_path: "./",
341                 static_root_path: shared.static_root_path.as_deref(),
342                 description: "List of crates",
343                 resource_suffix: &shared.resource_suffix,
344             };
345
346             let content = format!(
347                 "<h1>List of all crates</h1><ul class=\"all-items\">{}</ul>",
348                 krates
349                     .iter()
350                     .map(|s| {
351                         format!(
352                             "<li><a href=\"{}index.html\">{}</a></li>",
353                             ensure_trailing_slash(s),
354                             s
355                         )
356                     })
357                     .collect::<String>()
358             );
359             let v = layout::render(&shared.layout, &page, "", content, &shared.style_files);
360             shared.fs.write(dst, v)?;
361         }
362     }
363
364     // Update the list of all implementors for traits
365     let dst = cx.dst.join("implementors");
366     let cache = cx.cache();
367     for (&did, imps) in &cache.implementors {
368         // Private modules can leak through to this phase of rustdoc, which
369         // could contain implementations for otherwise private types. In some
370         // rare cases we could find an implementation for an item which wasn't
371         // indexed, so we just skip this step in that case.
372         //
373         // FIXME: this is a vague explanation for why this can't be a `get`, in
374         //        theory it should be...
375         let (remote_path, remote_item_type) = match cache.exact_paths.get(&did) {
376             Some(p) => match cache.paths.get(&did).or_else(|| cache.external_paths.get(&did)) {
377                 Some((_, t)) => (p, t),
378                 None => continue,
379             },
380             None => match cache.external_paths.get(&did) {
381                 Some((p, t)) => (p, t),
382                 None => continue,
383             },
384         };
385
386         struct Implementor {
387             text: String,
388             synthetic: bool,
389             types: Vec<String>,
390         }
391
392         impl Serialize for Implementor {
393             fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
394             where
395                 S: Serializer,
396             {
397                 let mut seq = serializer.serialize_seq(None)?;
398                 seq.serialize_element(&self.text)?;
399                 if self.synthetic {
400                     seq.serialize_element(&1)?;
401                     seq.serialize_element(&self.types)?;
402                 }
403                 seq.end()
404             }
405         }
406
407         let implementors = imps
408             .iter()
409             .filter_map(|imp| {
410                 // If the trait and implementation are in the same crate, then
411                 // there's no need to emit information about it (there's inlining
412                 // going on). If they're in different crates then the crate defining
413                 // the trait will be interested in our implementation.
414                 //
415                 // If the implementation is from another crate then that crate
416                 // should add it.
417                 if imp.impl_item.item_id.krate() == did.krate || !imp.impl_item.item_id.is_local() {
418                     None
419                 } else {
420                     Some(Implementor {
421                         text: imp.inner_impl().print(false, cx).to_string(),
422                         synthetic: imp.inner_impl().kind.is_auto(),
423                         types: collect_paths_for_type(imp.inner_impl().for_.clone(), cache),
424                     })
425                 }
426             })
427             .collect::<Vec<_>>();
428
429         // Only create a js file if we have impls to add to it. If the trait is
430         // documented locally though we always create the file to avoid dead
431         // links.
432         if implementors.is_empty() && !cache.paths.contains_key(&did) {
433             continue;
434         }
435
436         let implementors = format!(
437             r#""{}":{}"#,
438             krate.name(cx.tcx()),
439             serde_json::to_string(&implementors).expect("failed serde conversion"),
440         );
441
442         let mut mydst = dst.clone();
443         for part in &remote_path[..remote_path.len() - 1] {
444             mydst.push(part.to_string());
445         }
446         cx.shared.ensure_dir(&mydst)?;
447         mydst.push(&format!("{}.{}.js", remote_item_type, remote_path[remote_path.len() - 1]));
448
449         let (mut all_implementors, _) =
450             try_err!(collect(&mydst, krate.name(cx.tcx()).as_str()), &mydst);
451         all_implementors.push(implementors);
452         // Sort the implementors by crate so the file will be generated
453         // identically even with rustdoc running in parallel.
454         all_implementors.sort();
455
456         let mut v = String::from("(function() {var implementors = {\n");
457         v.push_str(&all_implementors.join(",\n"));
458         v.push_str("\n};");
459         v.push_str(
460             "if (window.register_implementors) {\
461                  window.register_implementors(implementors);\
462              } else {\
463                  window.pending_implementors = implementors;\
464              }",
465         );
466         v.push_str("})()");
467         cx.shared.fs.write(mydst, v)?;
468     }
469     Ok(())
470 }