]> git.lizzy.rs Git - rust.git/blob - src/librustdoc/html/render/context.rs
Rollup merge of #83835 - notriddle:sort-index, r=ollie27
[rust.git] / src / librustdoc / html / render / context.rs
1 use std::cell::RefCell;
2 use std::collections::BTreeMap;
3 use std::io;
4 use std::path::PathBuf;
5 use std::rc::Rc;
6 use std::sync::mpsc::channel;
7
8 use rustc_data_structures::fx::FxHashMap;
9 use rustc_hir::def_id::{DefId, LOCAL_CRATE};
10 use rustc_middle::ty::TyCtxt;
11 use rustc_session::Session;
12 use rustc_span::edition::Edition;
13 use rustc_span::source_map::FileName;
14 use rustc_span::{symbol::sym, Symbol};
15
16 use super::cache::{build_index, ExternalLocation};
17 use super::print_item::{full_path, item_path, print_item};
18 use super::write_shared::write_shared;
19 use super::{
20     print_sidebar, settings, AllTypes, NameDoc, SharedContext, StylePath, BASIC_KEYWORDS,
21     CURRENT_DEPTH,
22 };
23
24 use crate::clean::{self, AttributesExt};
25 use crate::config::RenderOptions;
26 use crate::docfs::{DocFS, PathError};
27 use crate::error::Error;
28 use crate::formats::cache::Cache;
29 use crate::formats::item_type::ItemType;
30 use crate::formats::FormatRenderer;
31 use crate::html::escape::Escape;
32 use crate::html::format::Buffer;
33 use crate::html::markdown::{self, plain_text_summary, ErrorCodes, IdMap};
34 use crate::html::{layout, sources};
35
36 /// Major driving force in all rustdoc rendering. This contains information
37 /// about where in the tree-like hierarchy rendering is occurring and controls
38 /// how the current page is being rendered.
39 ///
40 /// It is intended that this context is a lightweight object which can be fairly
41 /// easily cloned because it is cloned per work-job (about once per item in the
42 /// rustdoc tree).
43 crate struct Context<'tcx> {
44     /// Current hierarchy of components leading down to what's currently being
45     /// rendered
46     pub(super) current: Vec<String>,
47     /// The current destination folder of where HTML artifacts should be placed.
48     /// This changes as the context descends into the module hierarchy.
49     pub(super) dst: PathBuf,
50     /// A flag, which when `true`, will render pages which redirect to the
51     /// real location of an item. This is used to allow external links to
52     /// publicly reused items to redirect to the right location.
53     pub(super) render_redirect_pages: bool,
54     /// The map used to ensure all generated 'id=' attributes are unique.
55     pub(super) id_map: RefCell<IdMap>,
56     /// Tracks section IDs for `Deref` targets so they match in both the main
57     /// body and the sidebar.
58     pub(super) deref_id_map: RefCell<FxHashMap<DefId, String>>,
59     /// Shared mutable state.
60     ///
61     /// Issue for improving the situation: [#82381][]
62     ///
63     /// [#82381]: https://github.com/rust-lang/rust/issues/82381
64     pub(super) shared: Rc<SharedContext<'tcx>>,
65     /// The [`Cache`] used during rendering.
66     ///
67     /// Ideally the cache would be in [`SharedContext`], but it's mutated
68     /// between when the `SharedContext` is created and when `Context`
69     /// is created, so more refactoring would be needed.
70     ///
71     /// It's immutable once in `Context`, so it's not as bad that it's not in
72     /// `SharedContext`.
73     // FIXME: move `cache` to `SharedContext`
74     pub(super) cache: Rc<Cache>,
75 }
76
77 // `Context` is cloned a lot, so we don't want the size to grow unexpectedly.
78 #[cfg(target_arch = "x86_64")]
79 rustc_data_structures::static_assert_size!(Context<'_>, 152);
80
81 impl<'tcx> Context<'tcx> {
82     pub(super) fn tcx(&self) -> TyCtxt<'tcx> {
83         self.shared.tcx
84     }
85
86     fn sess(&self) -> &'tcx Session {
87         &self.shared.tcx.sess
88     }
89
90     pub(super) fn derive_id(&self, id: String) -> String {
91         let mut map = self.id_map.borrow_mut();
92         map.derive(id)
93     }
94
95     /// String representation of how to get back to the root path of the 'doc/'
96     /// folder in terms of a relative URL.
97     pub(super) fn root_path(&self) -> String {
98         "../".repeat(self.current.len())
99     }
100
101     fn render_item(&self, it: &clean::Item, pushname: bool) -> String {
102         // A little unfortunate that this is done like this, but it sure
103         // does make formatting *a lot* nicer.
104         CURRENT_DEPTH.with(|slot| {
105             slot.set(self.current.len());
106         });
107
108         let mut title = if it.is_primitive() || it.is_keyword() {
109             // No need to include the namespace for primitive types and keywords
110             String::new()
111         } else {
112             self.current.join("::")
113         };
114         if pushname {
115             if !title.is_empty() {
116                 title.push_str("::");
117             }
118             title.push_str(&it.name.unwrap().as_str());
119         }
120         title.push_str(" - Rust");
121         let tyname = it.type_();
122         let desc = it.doc_value().as_ref().map(|doc| plain_text_summary(&doc));
123         let desc = if let Some(desc) = desc {
124             desc
125         } else if it.is_crate() {
126             format!("API documentation for the Rust `{}` crate.", self.shared.layout.krate)
127         } else {
128             format!(
129                 "API documentation for the Rust `{}` {} in crate `{}`.",
130                 it.name.as_ref().unwrap(),
131                 tyname,
132                 self.shared.layout.krate
133             )
134         };
135         let keywords = make_item_keywords(it);
136         let page = layout::Page {
137             css_class: tyname.as_str(),
138             root_path: &self.root_path(),
139             static_root_path: self.shared.static_root_path.as_deref(),
140             title: &title,
141             description: &desc,
142             keywords: &keywords,
143             resource_suffix: &self.shared.resource_suffix,
144             extra_scripts: &[],
145             static_extra_scripts: &[],
146         };
147
148         if !self.render_redirect_pages {
149             layout::render(
150                 &self.shared.layout,
151                 &page,
152                 |buf: &mut _| print_sidebar(self, it, buf),
153                 |buf: &mut _| print_item(self, it, buf),
154                 &self.shared.style_files,
155             )
156         } else {
157             if let Some(&(ref names, ty)) = self.cache.paths.get(&it.def_id) {
158                 let mut path = String::new();
159                 for name in &names[..names.len() - 1] {
160                     path.push_str(name);
161                     path.push('/');
162                 }
163                 path.push_str(&item_path(ty, names.last().unwrap()));
164                 match self.shared.redirections {
165                     Some(ref redirections) => {
166                         let mut current_path = String::new();
167                         for name in &self.current {
168                             current_path.push_str(name);
169                             current_path.push('/');
170                         }
171                         current_path.push_str(&item_path(ty, names.last().unwrap()));
172                         redirections.borrow_mut().insert(current_path, path);
173                     }
174                     None => return layout::redirect(&format!("{}{}", self.root_path(), path)),
175                 }
176             }
177             String::new()
178         }
179     }
180
181     /// Construct a map of items shown in the sidebar to a plain-text summary of their docs.
182     fn build_sidebar_items(&self, m: &clean::Module) -> BTreeMap<String, Vec<NameDoc>> {
183         // BTreeMap instead of HashMap to get a sorted output
184         let mut map: BTreeMap<_, Vec<_>> = BTreeMap::new();
185         for item in &m.items {
186             if item.is_stripped() {
187                 continue;
188             }
189
190             let short = item.type_();
191             let myname = match item.name {
192                 None => continue,
193                 Some(ref s) => s.to_string(),
194             };
195             let short = short.to_string();
196             map.entry(short).or_default().push((
197                 myname,
198                 Some(item.doc_value().map_or_else(String::new, |s| plain_text_summary(&s))),
199             ));
200         }
201
202         if self.shared.sort_modules_alphabetically {
203             for items in map.values_mut() {
204                 items.sort();
205             }
206         }
207         map
208     }
209
210     /// Generates a url appropriate for an `href` attribute back to the source of
211     /// this item.
212     ///
213     /// The url generated, when clicked, will redirect the browser back to the
214     /// original source code.
215     ///
216     /// If `None` is returned, then a source link couldn't be generated. This
217     /// may happen, for example, with externally inlined items where the source
218     /// of their crate documentation isn't known.
219     pub(super) fn src_href(&self, item: &clean::Item) -> Option<String> {
220         if item.span.is_dummy() {
221             return None;
222         }
223         let mut root = self.root_path();
224         let mut path = String::new();
225         let cnum = item.span.cnum(self.sess());
226
227         // We can safely ignore synthetic `SourceFile`s.
228         let file = match item.span.filename(self.sess()) {
229             FileName::Real(ref path) => path.local_path().to_path_buf(),
230             _ => return None,
231         };
232         let file = &file;
233
234         let symbol;
235         let (krate, path) = if cnum == LOCAL_CRATE {
236             if let Some(path) = self.shared.local_sources.get(file) {
237                 (self.shared.layout.krate.as_str(), path)
238             } else {
239                 return None;
240             }
241         } else {
242             let (krate, src_root) = match *self.cache.extern_locations.get(&cnum)? {
243                 (name, ref src, ExternalLocation::Local) => (name, src),
244                 (name, ref src, ExternalLocation::Remote(ref s)) => {
245                     root = s.to_string();
246                     (name, src)
247                 }
248                 (_, _, ExternalLocation::Unknown) => return None,
249             };
250
251             sources::clean_path(&src_root, file, false, |component| {
252                 path.push_str(&component.to_string_lossy());
253                 path.push('/');
254             });
255             let mut fname = file.file_name().expect("source has no filename").to_os_string();
256             fname.push(".html");
257             path.push_str(&fname.to_string_lossy());
258             symbol = krate.as_str();
259             (&*symbol, &path)
260         };
261
262         let loline = item.span.lo(self.sess()).line;
263         let hiline = item.span.hi(self.sess()).line;
264         let lines =
265             if loline == hiline { loline.to_string() } else { format!("{}-{}", loline, hiline) };
266         Some(format!(
267             "{root}src/{krate}/{path}#{lines}",
268             root = Escape(&root),
269             krate = krate,
270             path = path,
271             lines = lines
272         ))
273     }
274 }
275
276 /// Generates the documentation for `crate` into the directory `dst`
277 impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> {
278     fn descr() -> &'static str {
279         "html"
280     }
281
282     const RUN_ON_MODULE: bool = true;
283
284     fn init(
285         mut krate: clean::Crate,
286         options: RenderOptions,
287         edition: Edition,
288         mut cache: Cache,
289         tcx: TyCtxt<'tcx>,
290     ) -> Result<(Self, clean::Crate), Error> {
291         // need to save a copy of the options for rendering the index page
292         let md_opts = options.clone();
293         let emit_crate = options.should_emit_crate();
294         let RenderOptions {
295             output,
296             external_html,
297             id_map,
298             playground_url,
299             sort_modules_alphabetically,
300             themes: style_files,
301             default_settings,
302             extension_css,
303             resource_suffix,
304             static_root_path,
305             generate_search_filter,
306             unstable_features,
307             generate_redirect_map,
308             ..
309         } = options;
310
311         let src_root = match krate.src {
312             FileName::Real(ref p) => match p.local_path().parent() {
313                 Some(p) => p.to_path_buf(),
314                 None => PathBuf::new(),
315             },
316             _ => PathBuf::new(),
317         };
318         // If user passed in `--playground-url` arg, we fill in crate name here
319         let mut playground = None;
320         if let Some(url) = playground_url {
321             playground =
322                 Some(markdown::Playground { crate_name: Some(krate.name.to_string()), url });
323         }
324         let mut layout = layout::Layout {
325             logo: String::new(),
326             favicon: String::new(),
327             external_html,
328             default_settings,
329             krate: krate.name.to_string(),
330             css_file_extension: extension_css,
331             generate_search_filter,
332         };
333         let mut issue_tracker_base_url = None;
334         let mut include_sources = true;
335
336         // Crawl the crate attributes looking for attributes which control how we're
337         // going to emit HTML
338         for attr in krate.module.attrs.lists(sym::doc) {
339             match (attr.name_or_empty(), attr.value_str()) {
340                 (sym::html_favicon_url, Some(s)) => {
341                     layout.favicon = s.to_string();
342                 }
343                 (sym::html_logo_url, Some(s)) => {
344                     layout.logo = s.to_string();
345                 }
346                 (sym::html_playground_url, Some(s)) => {
347                     playground = Some(markdown::Playground {
348                         crate_name: Some(krate.name.to_string()),
349                         url: s.to_string(),
350                     });
351                 }
352                 (sym::issue_tracker_base_url, Some(s)) => {
353                     issue_tracker_base_url = Some(s.to_string());
354                 }
355                 (sym::html_no_source, None) if attr.is_word() => {
356                     include_sources = false;
357                 }
358                 _ => {}
359             }
360         }
361         let (sender, receiver) = channel();
362         let mut scx = SharedContext {
363             tcx,
364             collapsed: krate.collapsed,
365             src_root,
366             include_sources,
367             local_sources: Default::default(),
368             issue_tracker_base_url,
369             layout,
370             created_dirs: Default::default(),
371             sort_modules_alphabetically,
372             style_files,
373             resource_suffix,
374             static_root_path,
375             fs: DocFS::new(sender),
376             edition,
377             codes: ErrorCodes::from(unstable_features.is_nightly_build()),
378             playground,
379             all: RefCell::new(AllTypes::new()),
380             errors: receiver,
381             redirections: if generate_redirect_map { Some(Default::default()) } else { None },
382         };
383
384         // Add the default themes to the `Vec` of stylepaths
385         //
386         // Note that these must be added before `sources::render` is called
387         // so that the resulting source pages are styled
388         //
389         // `light.css` is not disabled because it is the stylesheet that stays loaded
390         // by the browser as the theme stylesheet. The theme system (hackily) works by
391         // changing the href to this stylesheet. All other themes are disabled to
392         // prevent rule conflicts
393         scx.style_files.push(StylePath { path: PathBuf::from("light.css"), disabled: false });
394         scx.style_files.push(StylePath { path: PathBuf::from("dark.css"), disabled: true });
395         scx.style_files.push(StylePath { path: PathBuf::from("ayu.css"), disabled: true });
396
397         let dst = output;
398         scx.ensure_dir(&dst)?;
399         if emit_crate {
400             krate = sources::render(&dst, &mut scx, krate)?;
401         }
402
403         // Build our search index
404         let index = build_index(&krate, &mut cache, tcx);
405
406         let mut cx = Context {
407             current: Vec::new(),
408             dst,
409             render_redirect_pages: false,
410             id_map: RefCell::new(id_map),
411             deref_id_map: RefCell::new(FxHashMap::default()),
412             shared: Rc::new(scx),
413             cache: Rc::new(cache),
414         };
415
416         CURRENT_DEPTH.with(|s| s.set(0));
417
418         // Write shared runs within a flock; disable thread dispatching of IO temporarily.
419         Rc::get_mut(&mut cx.shared).unwrap().fs.set_sync_only(true);
420         write_shared(&cx, &krate, index, &md_opts)?;
421         Rc::get_mut(&mut cx.shared).unwrap().fs.set_sync_only(false);
422         Ok((cx, krate))
423     }
424
425     fn make_child_renderer(&self) -> Self {
426         Self {
427             current: self.current.clone(),
428             dst: self.dst.clone(),
429             render_redirect_pages: self.render_redirect_pages,
430             id_map: RefCell::new(IdMap::new()),
431             deref_id_map: RefCell::new(FxHashMap::default()),
432             shared: Rc::clone(&self.shared),
433             cache: Rc::clone(&self.cache),
434         }
435     }
436
437     fn after_krate(
438         &mut self,
439         crate_name: Symbol,
440         diag: &rustc_errors::Handler,
441     ) -> Result<(), Error> {
442         let final_file = self.dst.join(&*crate_name.as_str()).join("all.html");
443         let settings_file = self.dst.join("settings.html");
444
445         let mut root_path = self.dst.to_str().expect("invalid path").to_owned();
446         if !root_path.ends_with('/') {
447             root_path.push('/');
448         }
449         let mut page = layout::Page {
450             title: "List of all items in this crate",
451             css_class: "mod",
452             root_path: "../",
453             static_root_path: self.shared.static_root_path.as_deref(),
454             description: "List of all items in this crate",
455             keywords: BASIC_KEYWORDS,
456             resource_suffix: &self.shared.resource_suffix,
457             extra_scripts: &[],
458             static_extra_scripts: &[],
459         };
460         let sidebar = if let Some(ref version) = self.cache.crate_version {
461             format!(
462                 "<p class=\"location\">Crate {}</p>\
463                      <div class=\"block version\">\
464                          <p>Version {}</p>\
465                      </div>\
466                      <a id=\"all-types\" href=\"index.html\"><p>Back to index</p></a>",
467                 crate_name,
468                 Escape(version),
469             )
470         } else {
471             String::new()
472         };
473         let all = self.shared.all.replace(AllTypes::new());
474         let v = layout::render(
475             &self.shared.layout,
476             &page,
477             sidebar,
478             |buf: &mut Buffer| all.print(buf),
479             &self.shared.style_files,
480         );
481         self.shared.fs.write(final_file, v.as_bytes())?;
482
483         // Generating settings page.
484         page.title = "Rustdoc settings";
485         page.description = "Settings of Rustdoc";
486         page.root_path = "./";
487
488         let mut style_files = self.shared.style_files.clone();
489         let sidebar = "<p class=\"location\">Settings</p><div class=\"sidebar-elems\"></div>";
490         style_files.push(StylePath { path: PathBuf::from("settings.css"), disabled: false });
491         let v = layout::render(
492             &self.shared.layout,
493             &page,
494             sidebar,
495             settings(
496                 self.shared.static_root_path.as_deref().unwrap_or("./"),
497                 &self.shared.resource_suffix,
498                 &self.shared.style_files,
499             )?,
500             &style_files,
501         );
502         self.shared.fs.write(&settings_file, v.as_bytes())?;
503         if let Some(ref redirections) = self.shared.redirections {
504             if !redirections.borrow().is_empty() {
505                 let redirect_map_path =
506                     self.dst.join(&*crate_name.as_str()).join("redirect-map.json");
507                 let paths = serde_json::to_string(&*redirections.borrow()).unwrap();
508                 self.shared.ensure_dir(&self.dst.join(&*crate_name.as_str()))?;
509                 self.shared.fs.write(&redirect_map_path, paths.as_bytes())?;
510             }
511         }
512
513         // Flush pending errors.
514         Rc::get_mut(&mut self.shared).unwrap().fs.close();
515         let nb_errors = self.shared.errors.iter().map(|err| diag.struct_err(&err).emit()).count();
516         if nb_errors > 0 {
517             Err(Error::new(io::Error::new(io::ErrorKind::Other, "I/O error"), ""))
518         } else {
519             Ok(())
520         }
521     }
522
523     fn mod_item_in(&mut self, item: &clean::Item, item_name: &str) -> Result<(), Error> {
524         // Stripped modules survive the rustdoc passes (i.e., `strip-private`)
525         // if they contain impls for public types. These modules can also
526         // contain items such as publicly re-exported structures.
527         //
528         // External crates will provide links to these structures, so
529         // these modules are recursed into, but not rendered normally
530         // (a flag on the context).
531         if !self.render_redirect_pages {
532             self.render_redirect_pages = item.is_stripped();
533         }
534         let scx = &self.shared;
535         self.dst.push(item_name);
536         self.current.push(item_name.to_owned());
537
538         info!("Recursing into {}", self.dst.display());
539
540         let buf = self.render_item(item, false);
541         // buf will be empty if the module is stripped and there is no redirect for it
542         if !buf.is_empty() {
543             self.shared.ensure_dir(&self.dst)?;
544             let joint_dst = self.dst.join("index.html");
545             scx.fs.write(&joint_dst, buf.as_bytes())?;
546         }
547
548         // Render sidebar-items.js used throughout this module.
549         if !self.render_redirect_pages {
550             let module = match *item.kind {
551                 clean::StrippedItem(box clean::ModuleItem(ref m)) | clean::ModuleItem(ref m) => m,
552                 _ => unreachable!(),
553             };
554             let items = self.build_sidebar_items(module);
555             let js_dst = self.dst.join("sidebar-items.js");
556             let v = format!("initSidebarItems({});", serde_json::to_string(&items).unwrap());
557             scx.fs.write(&js_dst, &v)?;
558         }
559         Ok(())
560     }
561
562     fn mod_item_out(&mut self, _item_name: &str) -> Result<(), Error> {
563         info!("Recursed; leaving {}", self.dst.display());
564
565         // Go back to where we were at
566         self.dst.pop();
567         self.current.pop();
568         Ok(())
569     }
570
571     fn item(&mut self, item: clean::Item) -> Result<(), Error> {
572         // Stripped modules survive the rustdoc passes (i.e., `strip-private`)
573         // if they contain impls for public types. These modules can also
574         // contain items such as publicly re-exported structures.
575         //
576         // External crates will provide links to these structures, so
577         // these modules are recursed into, but not rendered normally
578         // (a flag on the context).
579         if !self.render_redirect_pages {
580             self.render_redirect_pages = item.is_stripped();
581         }
582
583         let buf = self.render_item(&item, true);
584         // buf will be empty if the item is stripped and there is no redirect for it
585         if !buf.is_empty() {
586             let name = item.name.as_ref().unwrap();
587             let item_type = item.type_();
588             let file_name = &item_path(item_type, &name.as_str());
589             self.shared.ensure_dir(&self.dst)?;
590             let joint_dst = self.dst.join(file_name);
591             self.shared.fs.write(&joint_dst, buf.as_bytes())?;
592
593             if !self.render_redirect_pages {
594                 self.shared.all.borrow_mut().append(full_path(self, &item), &item_type);
595             }
596             // If the item is a macro, redirect from the old macro URL (with !)
597             // to the new one (without).
598             if item_type == ItemType::Macro {
599                 let redir_name = format!("{}.{}!.html", item_type, name);
600                 if let Some(ref redirections) = self.shared.redirections {
601                     let crate_name = &self.shared.layout.krate;
602                     redirections.borrow_mut().insert(
603                         format!("{}/{}", crate_name, redir_name),
604                         format!("{}/{}", crate_name, file_name),
605                     );
606                 } else {
607                     let v = layout::redirect(file_name);
608                     let redir_dst = self.dst.join(redir_name);
609                     self.shared.fs.write(&redir_dst, v.as_bytes())?;
610                 }
611             }
612         }
613         Ok(())
614     }
615
616     fn cache(&self) -> &Cache {
617         &self.cache
618     }
619 }
620
621 fn make_item_keywords(it: &clean::Item) -> String {
622     format!("{}, {}", BASIC_KEYWORDS, it.name.as_ref().unwrap())
623 }