2021-02-13 21:28:34 -08:00
|
|
|
use std::cell::RefCell;
|
|
|
|
|
use std::collections::BTreeMap;
|
|
|
|
|
use std::io;
|
2021-04-02 12:08:28 -07:00
|
|
|
use std::path::{Path, PathBuf};
|
2021-02-13 21:28:34 -08:00
|
|
|
use std::rc::Rc;
|
2021-04-02 12:08:28 -07:00
|
|
|
use std::sync::mpsc::{channel, Receiver};
|
2021-02-13 21:28:34 -08:00
|
|
|
|
2021-04-02 12:08:28 -07:00
|
|
|
use rustc_data_structures::fx::{FxHashMap, FxHashSet};
|
2021-10-22 21:45:06 +02:00
|
|
|
use rustc_hir::def_id::{DefId, LOCAL_CRATE};
|
2021-02-13 21:28:34 -08:00
|
|
|
use rustc_middle::ty::TyCtxt;
|
|
|
|
|
use rustc_session::Session;
|
|
|
|
|
use rustc_span::edition::Edition;
|
|
|
|
|
use rustc_span::source_map::FileName;
|
2021-12-15 06:18:18 +11:00
|
|
|
use rustc_span::{sym, Symbol};
|
2021-02-13 21:28:34 -08:00
|
|
|
|
2021-02-13 22:23:05 -08:00
|
|
|
use super::print_item::{full_path, item_path, print_item};
|
2021-12-27 18:57:07 -08:00
|
|
|
use super::search_index::build_index;
|
2021-02-13 23:17:38 -08:00
|
|
|
use super::write_shared::write_shared;
|
2021-04-06 00:07:46 +02:00
|
|
|
use super::{
|
2021-08-02 17:44:04 +02:00
|
|
|
collect_spans_and_sources, print_sidebar, settings, AllTypes, LinkFromSrc, NameDoc, StylePath,
|
|
|
|
|
BASIC_KEYWORDS,
|
2021-04-06 00:07:46 +02:00
|
|
|
};
|
2021-02-13 21:28:34 -08:00
|
|
|
|
2021-12-27 18:57:07 -08:00
|
|
|
use crate::clean::{self, types::ExternalLocation, ExternalCrate};
|
2021-02-13 21:28:34 -08:00
|
|
|
use crate::config::RenderOptions;
|
|
|
|
|
use crate::docfs::{DocFS, PathError};
|
|
|
|
|
use crate::error::Error;
|
|
|
|
|
use crate::formats::cache::Cache;
|
|
|
|
|
use crate::formats::item_type::ItemType;
|
|
|
|
|
use crate::formats::FormatRenderer;
|
|
|
|
|
use crate::html::escape::Escape;
|
2021-12-15 06:18:18 +11:00
|
|
|
use crate::html::format::{join_with_double_colon, Buffer};
|
2021-02-22 15:43:43 -08:00
|
|
|
use crate::html::markdown::{self, plain_text_summary, ErrorCodes, IdMap};
|
2021-02-13 21:28:34 -08:00
|
|
|
use crate::html::{layout, sources};
|
2021-09-16 18:12:45 -07:00
|
|
|
use crate::scrape_examples::AllCallLocations;
|
2021-10-30 02:07:37 +00:00
|
|
|
use crate::try_err;
|
2021-02-13 21:28:34 -08:00
|
|
|
|
|
|
|
|
/// Major driving force in all rustdoc rendering. This contains information
|
|
|
|
|
/// about where in the tree-like hierarchy rendering is occurring and controls
|
|
|
|
|
/// how the current page is being rendered.
|
|
|
|
|
///
|
|
|
|
|
/// It is intended that this context is a lightweight object which can be fairly
|
|
|
|
|
/// easily cloned because it is cloned per work-job (about once per item in the
|
|
|
|
|
/// rustdoc tree).
|
|
|
|
|
crate struct Context<'tcx> {
|
|
|
|
|
/// Current hierarchy of components leading down to what's currently being
|
|
|
|
|
/// rendered
|
2021-12-15 06:18:18 +11:00
|
|
|
pub(crate) current: Vec<Symbol>,
|
2021-02-13 21:28:34 -08:00
|
|
|
/// The current destination folder of where HTML artifacts should be placed.
|
|
|
|
|
/// This changes as the context descends into the module hierarchy.
|
2021-04-06 00:07:46 +02:00
|
|
|
crate dst: PathBuf,
|
2021-02-13 21:28:34 -08:00
|
|
|
/// A flag, which when `true`, will render pages which redirect to the
|
|
|
|
|
/// real location of an item. This is used to allow external links to
|
|
|
|
|
/// publicly reused items to redirect to the right location.
|
2021-02-21 14:35:15 -08:00
|
|
|
pub(super) render_redirect_pages: bool,
|
2021-10-22 21:45:06 +02:00
|
|
|
/// Tracks section IDs for `Deref` targets so they match in both the main
|
|
|
|
|
/// body and the sidebar.
|
2021-10-23 22:18:33 +02:00
|
|
|
pub(super) deref_id_map: RefCell<FxHashMap<DefId, String>>,
|
2021-02-22 15:43:43 -08:00
|
|
|
/// The map used to ensure all generated 'id=' attributes are unique.
|
2021-03-01 20:06:49 -08:00
|
|
|
pub(super) id_map: RefCell<IdMap>,
|
2021-02-21 14:35:15 -08:00
|
|
|
/// Shared mutable state.
|
|
|
|
|
///
|
|
|
|
|
/// Issue for improving the situation: [#82381][]
|
|
|
|
|
///
|
|
|
|
|
/// [#82381]: https://github.com/rust-lang/rust/issues/82381
|
2021-04-06 00:07:46 +02:00
|
|
|
crate shared: Rc<SharedContext<'tcx>>,
|
2021-12-03 17:09:04 -08:00
|
|
|
/// This flag indicates whether source links should be generated or not. If
|
2021-04-06 00:07:46 +02:00
|
|
|
/// the source files are present in the html rendering, then this will be
|
|
|
|
|
/// `true`.
|
|
|
|
|
crate include_sources: bool,
|
2021-02-13 21:28:34 -08:00
|
|
|
}
|
|
|
|
|
|
2021-02-21 17:19:43 -08:00
|
|
|
// `Context` is cloned a lot, so we don't want the size to grow unexpectedly.
|
2021-09-05 16:42:36 +01:00
|
|
|
#[cfg(all(target_arch = "x86_64", target_pointer_width = "64"))]
|
2021-10-23 22:18:33 +02:00
|
|
|
rustc_data_structures::static_assert_size!(Context<'_>, 144);
|
2021-02-21 17:19:43 -08:00
|
|
|
|
2021-04-02 12:08:28 -07:00
|
|
|
/// Shared mutable state used in [`Context`] and elsewhere.
|
|
|
|
|
crate struct SharedContext<'tcx> {
|
|
|
|
|
crate tcx: TyCtxt<'tcx>,
|
|
|
|
|
/// The path to the crate root source minus the file name.
|
|
|
|
|
/// Used for simplifying paths to the highlighted source code files.
|
|
|
|
|
crate src_root: PathBuf,
|
|
|
|
|
/// This describes the layout of each page, and is not modified after
|
|
|
|
|
/// creation of the context (contains info like the favicon and added html).
|
|
|
|
|
crate layout: layout::Layout,
|
|
|
|
|
/// The local file sources we've emitted and their respective url-paths.
|
|
|
|
|
crate local_sources: FxHashMap<PathBuf, String>,
|
2021-04-12 19:42:26 -07:00
|
|
|
/// Show the memory layout of types in the docs.
|
|
|
|
|
pub(super) show_type_layout: bool,
|
2021-04-02 12:08:28 -07:00
|
|
|
/// The base-URL of the issue tracker for when an item has been tagged with
|
|
|
|
|
/// an issue number.
|
|
|
|
|
pub(super) issue_tracker_base_url: Option<String>,
|
|
|
|
|
/// The directories that have already been created in this doc run. Used to reduce the number
|
|
|
|
|
/// of spurious `create_dir_all` calls.
|
|
|
|
|
created_dirs: RefCell<FxHashSet<PathBuf>>,
|
|
|
|
|
/// This flag indicates whether listings of modules (in the side bar and documentation itself)
|
|
|
|
|
/// should be ordered alphabetically or in order of appearance (in the source code).
|
|
|
|
|
pub(super) sort_modules_alphabetically: bool,
|
|
|
|
|
/// Additional CSS files to be added to the generated docs.
|
|
|
|
|
crate style_files: Vec<StylePath>,
|
|
|
|
|
/// Suffix to be added on resource files (if suffix is "-v2" then "light.css" becomes
|
|
|
|
|
/// "light-v2.css").
|
|
|
|
|
crate resource_suffix: String,
|
|
|
|
|
/// Optional path string to be used to load static files on output pages. If not set, uses
|
|
|
|
|
/// combinations of `../` to reach the documentation root.
|
|
|
|
|
crate static_root_path: Option<String>,
|
|
|
|
|
/// The fs handle we are working with.
|
|
|
|
|
crate fs: DocFS,
|
|
|
|
|
pub(super) codes: ErrorCodes,
|
|
|
|
|
pub(super) playground: Option<markdown::Playground>,
|
|
|
|
|
all: RefCell<AllTypes>,
|
|
|
|
|
/// Storage for the errors produced while generating documentation so they
|
|
|
|
|
/// can be printed together at the end.
|
|
|
|
|
errors: Receiver<String>,
|
|
|
|
|
/// `None` by default, depends on the `generate-redirect-map` option flag. If this field is set
|
|
|
|
|
/// to `Some(...)`, it'll store redirections and then generate a JSON file at the top level of
|
|
|
|
|
/// the crate.
|
|
|
|
|
redirections: Option<RefCell<FxHashMap<String, String>>>,
|
2021-06-08 17:19:03 -07:00
|
|
|
|
2021-04-06 00:07:46 +02:00
|
|
|
/// Correspondance map used to link types used in the source code pages to allow to click on
|
|
|
|
|
/// links to jump to the type's definition.
|
2021-08-02 17:44:04 +02:00
|
|
|
crate span_correspondance_map: FxHashMap<rustc_span::Span, LinkFromSrc>,
|
2021-08-22 03:36:37 +00:00
|
|
|
/// The [`Cache`] used during rendering.
|
|
|
|
|
crate cache: Cache,
|
2021-09-16 18:12:45 -07:00
|
|
|
|
|
|
|
|
crate call_locations: AllCallLocations,
|
2021-04-02 12:08:28 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl SharedContext<'_> {
|
|
|
|
|
crate fn ensure_dir(&self, dst: &Path) -> Result<(), Error> {
|
|
|
|
|
let mut dirs = self.created_dirs.borrow_mut();
|
|
|
|
|
if !dirs.contains(dst) {
|
|
|
|
|
try_err!(self.fs.create_dir_all(dst), dst);
|
|
|
|
|
dirs.insert(dst.to_path_buf());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-22 19:38:20 -04:00
|
|
|
crate fn edition(&self) -> Edition {
|
|
|
|
|
self.tcx.sess.edition()
|
|
|
|
|
}
|
2021-04-02 12:08:28 -07:00
|
|
|
}
|
|
|
|
|
|
2021-02-13 21:28:34 -08:00
|
|
|
impl<'tcx> Context<'tcx> {
|
2021-03-17 11:41:01 -07:00
|
|
|
pub(crate) fn tcx(&self) -> TyCtxt<'tcx> {
|
2021-02-13 21:28:34 -08:00
|
|
|
self.shared.tcx
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-17 11:41:01 -07:00
|
|
|
pub(crate) fn cache(&self) -> &Cache {
|
2021-08-22 03:36:37 +00:00
|
|
|
&self.shared.cache
|
2021-03-17 11:41:01 -07:00
|
|
|
}
|
|
|
|
|
|
2021-04-22 13:28:43 -04:00
|
|
|
pub(super) fn sess(&self) -> &'tcx Session {
|
2021-10-01 17:12:39 +02:00
|
|
|
self.shared.tcx.sess
|
2021-02-13 21:28:34 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) fn derive_id(&self, id: String) -> String {
|
2021-02-22 15:43:43 -08:00
|
|
|
let mut map = self.id_map.borrow_mut();
|
2021-02-13 21:28:34 -08:00
|
|
|
map.derive(id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// String representation of how to get back to the root path of the 'doc/'
|
|
|
|
|
/// folder in terms of a relative URL.
|
|
|
|
|
pub(super) fn root_path(&self) -> String {
|
|
|
|
|
"../".repeat(self.current.len())
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-20 18:53:15 -04:00
|
|
|
fn render_item(&self, it: &clean::Item, is_module: bool) -> String {
|
2021-04-20 17:31:18 -04:00
|
|
|
let mut title = String::new();
|
2021-04-20 19:10:00 -04:00
|
|
|
if !is_module {
|
2021-12-15 14:39:23 +11:00
|
|
|
title.push_str(it.name.unwrap().as_str());
|
2021-02-13 21:28:34 -08:00
|
|
|
}
|
2021-04-20 17:31:18 -04:00
|
|
|
if !it.is_primitive() && !it.is_keyword() {
|
2021-04-20 19:10:00 -04:00
|
|
|
if !is_module {
|
2021-04-20 17:31:18 -04:00
|
|
|
title.push_str(" in ");
|
|
|
|
|
}
|
|
|
|
|
// No need to include the namespace for primitive types and keywords
|
2021-12-15 06:18:18 +11:00
|
|
|
title.push_str(&join_with_double_colon(&self.current));
|
2021-04-20 17:31:18 -04:00
|
|
|
};
|
2021-02-13 21:28:34 -08:00
|
|
|
title.push_str(" - Rust");
|
|
|
|
|
let tyname = it.type_();
|
2021-10-01 17:12:39 +02:00
|
|
|
let desc = it.doc_value().as_ref().map(|doc| plain_text_summary(doc));
|
2021-02-13 21:28:34 -08:00
|
|
|
let desc = if let Some(desc) = desc {
|
|
|
|
|
desc
|
|
|
|
|
} else if it.is_crate() {
|
|
|
|
|
format!("API documentation for the Rust `{}` crate.", self.shared.layout.krate)
|
|
|
|
|
} else {
|
|
|
|
|
format!(
|
|
|
|
|
"API documentation for the Rust `{}` {} in crate `{}`.",
|
|
|
|
|
it.name.as_ref().unwrap(),
|
|
|
|
|
tyname,
|
|
|
|
|
self.shared.layout.krate
|
|
|
|
|
)
|
|
|
|
|
};
|
|
|
|
|
let keywords = make_item_keywords(it);
|
2021-05-31 11:51:22 +02:00
|
|
|
let name;
|
|
|
|
|
let tyname_s = if it.is_crate() {
|
|
|
|
|
name = format!("{} crate", tyname);
|
|
|
|
|
name.as_str()
|
|
|
|
|
} else {
|
|
|
|
|
tyname.as_str()
|
|
|
|
|
};
|
2021-02-13 21:28:34 -08:00
|
|
|
let page = layout::Page {
|
2021-05-31 11:51:22 +02:00
|
|
|
css_class: tyname_s,
|
2021-02-13 21:28:34 -08:00
|
|
|
root_path: &self.root_path(),
|
|
|
|
|
static_root_path: self.shared.static_root_path.as_deref(),
|
|
|
|
|
title: &title,
|
|
|
|
|
description: &desc,
|
|
|
|
|
keywords: &keywords,
|
|
|
|
|
resource_suffix: &self.shared.resource_suffix,
|
|
|
|
|
extra_scripts: &[],
|
|
|
|
|
static_extra_scripts: &[],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if !self.render_redirect_pages {
|
|
|
|
|
layout::render(
|
|
|
|
|
&self.shared.layout,
|
|
|
|
|
&page,
|
|
|
|
|
|buf: &mut _| print_sidebar(self, it, buf),
|
2022-01-03 18:18:46 +01:00
|
|
|
|buf: &mut _| print_item(self, it, buf, &page),
|
2021-02-13 21:28:34 -08:00
|
|
|
&self.shared.style_files,
|
|
|
|
|
)
|
|
|
|
|
} else {
|
2021-08-22 03:36:37 +00:00
|
|
|
if let Some(&(ref names, ty)) = self.cache().paths.get(&it.def_id.expect_def_id()) {
|
2021-02-13 21:28:34 -08:00
|
|
|
let mut path = String::new();
|
|
|
|
|
for name in &names[..names.len() - 1] {
|
2021-12-15 06:18:18 +11:00
|
|
|
path.push_str(&name.as_str());
|
2021-02-13 21:28:34 -08:00
|
|
|
path.push('/');
|
|
|
|
|
}
|
2021-12-15 06:18:18 +11:00
|
|
|
path.push_str(&item_path(ty, &names.last().unwrap().as_str()));
|
2021-02-21 14:21:43 -08:00
|
|
|
match self.shared.redirections {
|
2021-02-13 21:28:34 -08:00
|
|
|
Some(ref redirections) => {
|
|
|
|
|
let mut current_path = String::new();
|
|
|
|
|
for name in &self.current {
|
2021-12-15 06:18:18 +11:00
|
|
|
current_path.push_str(&name.as_str());
|
2021-02-13 21:28:34 -08:00
|
|
|
current_path.push('/');
|
|
|
|
|
}
|
2021-12-15 06:18:18 +11:00
|
|
|
current_path.push_str(&item_path(ty, &names.last().unwrap().as_str()));
|
2021-02-13 21:28:34 -08:00
|
|
|
redirections.borrow_mut().insert(current_path, path);
|
|
|
|
|
}
|
|
|
|
|
None => return layout::redirect(&format!("{}{}", self.root_path(), path)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
String::new()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Construct a map of items shown in the sidebar to a plain-text summary of their docs.
|
|
|
|
|
fn build_sidebar_items(&self, m: &clean::Module) -> BTreeMap<String, Vec<NameDoc>> {
|
|
|
|
|
// BTreeMap instead of HashMap to get a sorted output
|
|
|
|
|
let mut map: BTreeMap<_, Vec<_>> = BTreeMap::new();
|
|
|
|
|
for item in &m.items {
|
|
|
|
|
if item.is_stripped() {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let short = item.type_();
|
|
|
|
|
let myname = match item.name {
|
|
|
|
|
None => continue,
|
|
|
|
|
Some(ref s) => s.to_string(),
|
|
|
|
|
};
|
|
|
|
|
let short = short.to_string();
|
|
|
|
|
map.entry(short).or_default().push((
|
|
|
|
|
myname,
|
|
|
|
|
Some(item.doc_value().map_or_else(String::new, |s| plain_text_summary(&s))),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.shared.sort_modules_alphabetically {
|
|
|
|
|
for items in map.values_mut() {
|
|
|
|
|
items.sort();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
map
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Generates a url appropriate for an `href` attribute back to the source of
|
|
|
|
|
/// this item.
|
|
|
|
|
///
|
|
|
|
|
/// The url generated, when clicked, will redirect the browser back to the
|
|
|
|
|
/// original source code.
|
|
|
|
|
///
|
|
|
|
|
/// If `None` is returned, then a source link couldn't be generated. This
|
|
|
|
|
/// may happen, for example, with externally inlined items where the source
|
|
|
|
|
/// of their crate documentation isn't known.
|
|
|
|
|
pub(super) fn src_href(&self, item: &clean::Item) -> Option<String> {
|
2021-10-07 10:27:09 -07:00
|
|
|
self.href_from_span(item.span(self.tcx()), true)
|
2021-04-06 00:07:46 +02:00
|
|
|
}
|
|
|
|
|
|
2021-10-07 10:27:09 -07:00
|
|
|
crate fn href_from_span(&self, span: clean::Span, with_lines: bool) -> Option<String> {
|
2021-04-06 00:07:46 +02:00
|
|
|
if span.is_dummy() {
|
2021-02-13 21:28:34 -08:00
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
let mut root = self.root_path();
|
|
|
|
|
let mut path = String::new();
|
2021-04-06 00:07:46 +02:00
|
|
|
let cnum = span.cnum(self.sess());
|
2021-02-13 21:28:34 -08:00
|
|
|
|
|
|
|
|
// We can safely ignore synthetic `SourceFile`s.
|
2021-04-06 00:07:46 +02:00
|
|
|
let file = match span.filename(self.sess()) {
|
2021-04-09 00:54:51 +01:00
|
|
|
FileName::Real(ref path) => path.local_path_if_available().to_path_buf(),
|
2021-02-13 21:28:34 -08:00
|
|
|
_ => return None,
|
|
|
|
|
};
|
|
|
|
|
let file = &file;
|
|
|
|
|
|
2021-12-15 08:32:21 +11:00
|
|
|
let krate_sym;
|
2021-02-13 21:28:34 -08:00
|
|
|
let (krate, path) = if cnum == LOCAL_CRATE {
|
|
|
|
|
if let Some(path) = self.shared.local_sources.get(file) {
|
|
|
|
|
(self.shared.layout.krate.as_str(), path)
|
|
|
|
|
} else {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2021-08-22 03:36:37 +00:00
|
|
|
let (krate, src_root) = match *self.cache().extern_locations.get(&cnum)? {
|
2021-04-29 19:14:29 +02:00
|
|
|
ExternalLocation::Local => {
|
|
|
|
|
let e = ExternalCrate { crate_num: cnum };
|
|
|
|
|
(e.name(self.tcx()), e.src_root(self.tcx()))
|
|
|
|
|
}
|
|
|
|
|
ExternalLocation::Remote(ref s) => {
|
2021-02-13 21:28:34 -08:00
|
|
|
root = s.to_string();
|
2021-04-29 19:14:29 +02:00
|
|
|
let e = ExternalCrate { crate_num: cnum };
|
|
|
|
|
(e.name(self.tcx()), e.src_root(self.tcx()))
|
2021-02-13 21:28:34 -08:00
|
|
|
}
|
2021-04-29 19:14:29 +02:00
|
|
|
ExternalLocation::Unknown => return None,
|
2021-02-13 21:28:34 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
sources::clean_path(&src_root, file, false, |component| {
|
|
|
|
|
path.push_str(&component.to_string_lossy());
|
|
|
|
|
path.push('/');
|
|
|
|
|
});
|
|
|
|
|
let mut fname = file.file_name().expect("source has no filename").to_os_string();
|
|
|
|
|
fname.push(".html");
|
|
|
|
|
path.push_str(&fname.to_string_lossy());
|
2021-12-15 08:32:21 +11:00
|
|
|
krate_sym = krate;
|
|
|
|
|
(krate_sym.as_str(), &path)
|
2021-02-13 21:28:34 -08:00
|
|
|
};
|
|
|
|
|
|
2021-10-07 10:27:09 -07:00
|
|
|
let anchor = if with_lines {
|
|
|
|
|
let loline = span.lo(self.sess()).line;
|
|
|
|
|
let hiline = span.hi(self.sess()).line;
|
|
|
|
|
format!(
|
|
|
|
|
"#{}",
|
|
|
|
|
if loline == hiline {
|
|
|
|
|
loline.to_string()
|
|
|
|
|
} else {
|
|
|
|
|
format!("{}-{}", loline, hiline)
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
"".to_string()
|
|
|
|
|
};
|
2021-02-13 21:28:34 -08:00
|
|
|
Some(format!(
|
2021-10-07 10:27:09 -07:00
|
|
|
"{root}src/{krate}/{path}{anchor}",
|
2021-02-13 21:28:34 -08:00
|
|
|
root = Escape(&root),
|
|
|
|
|
krate = krate,
|
|
|
|
|
path = path,
|
2021-10-07 10:27:09 -07:00
|
|
|
anchor = anchor
|
2021-02-13 21:28:34 -08:00
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Generates the documentation for `crate` into the directory `dst`
|
|
|
|
|
impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> {
|
|
|
|
|
fn descr() -> &'static str {
|
|
|
|
|
"html"
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-24 15:54:20 +00:00
|
|
|
const RUN_ON_MODULE: bool = true;
|
|
|
|
|
|
2021-02-13 21:28:34 -08:00
|
|
|
fn init(
|
2021-04-06 00:07:46 +02:00
|
|
|
krate: clean::Crate,
|
2021-02-13 21:28:34 -08:00
|
|
|
options: RenderOptions,
|
2021-04-06 00:07:46 +02:00
|
|
|
cache: Cache,
|
2021-02-13 21:28:34 -08:00
|
|
|
tcx: TyCtxt<'tcx>,
|
|
|
|
|
) -> Result<(Self, clean::Crate), Error> {
|
|
|
|
|
// need to save a copy of the options for rendering the index page
|
|
|
|
|
let md_opts = options.clone();
|
2021-03-25 12:46:35 -04:00
|
|
|
let emit_crate = options.should_emit_crate();
|
2021-02-13 21:28:34 -08:00
|
|
|
let RenderOptions {
|
|
|
|
|
output,
|
|
|
|
|
external_html,
|
|
|
|
|
id_map,
|
|
|
|
|
playground_url,
|
|
|
|
|
sort_modules_alphabetically,
|
|
|
|
|
themes: style_files,
|
|
|
|
|
default_settings,
|
|
|
|
|
extension_css,
|
|
|
|
|
resource_suffix,
|
|
|
|
|
static_root_path,
|
|
|
|
|
unstable_features,
|
|
|
|
|
generate_redirect_map,
|
2021-04-12 19:42:26 -07:00
|
|
|
show_type_layout,
|
2021-04-13 15:52:41 +02:00
|
|
|
generate_link_to_definition,
|
2021-09-16 18:12:45 -07:00
|
|
|
call_locations,
|
2021-12-11 10:13:16 -08:00
|
|
|
no_emit_shared,
|
2021-02-13 21:28:34 -08:00
|
|
|
..
|
|
|
|
|
} = options;
|
|
|
|
|
|
2021-10-28 20:48:48 -07:00
|
|
|
let src_root = match krate.src(tcx) {
|
2021-04-19 23:27:02 +01:00
|
|
|
FileName::Real(ref p) => match p.local_path_if_available().parent() {
|
2021-02-13 21:28:34 -08:00
|
|
|
Some(p) => p.to_path_buf(),
|
|
|
|
|
None => PathBuf::new(),
|
|
|
|
|
},
|
|
|
|
|
_ => PathBuf::new(),
|
|
|
|
|
};
|
|
|
|
|
// If user passed in `--playground-url` arg, we fill in crate name here
|
|
|
|
|
let mut playground = None;
|
|
|
|
|
if let Some(url) = playground_url {
|
|
|
|
|
playground =
|
2021-10-28 20:55:02 -07:00
|
|
|
Some(markdown::Playground { crate_name: Some(krate.name(tcx).to_string()), url });
|
2021-02-13 21:28:34 -08:00
|
|
|
}
|
|
|
|
|
let mut layout = layout::Layout {
|
|
|
|
|
logo: String::new(),
|
|
|
|
|
favicon: String::new(),
|
|
|
|
|
external_html,
|
|
|
|
|
default_settings,
|
2021-10-28 20:55:02 -07:00
|
|
|
krate: krate.name(tcx).to_string(),
|
2021-02-13 21:28:34 -08:00
|
|
|
css_file_extension: extension_css,
|
2021-10-01 13:57:57 -07:00
|
|
|
scrape_examples_extension: !call_locations.is_empty(),
|
2021-02-13 21:28:34 -08:00
|
|
|
};
|
|
|
|
|
let mut issue_tracker_base_url = None;
|
|
|
|
|
let mut include_sources = true;
|
2021-06-08 17:19:03 -07:00
|
|
|
|
2021-02-13 21:28:34 -08:00
|
|
|
// Crawl the crate attributes looking for attributes which control how we're
|
|
|
|
|
// going to emit HTML
|
2021-03-23 11:04:09 -07:00
|
|
|
for attr in krate.module.attrs.lists(sym::doc) {
|
|
|
|
|
match (attr.name_or_empty(), attr.value_str()) {
|
|
|
|
|
(sym::html_favicon_url, Some(s)) => {
|
|
|
|
|
layout.favicon = s.to_string();
|
|
|
|
|
}
|
|
|
|
|
(sym::html_logo_url, Some(s)) => {
|
|
|
|
|
layout.logo = s.to_string();
|
|
|
|
|
}
|
|
|
|
|
(sym::html_playground_url, Some(s)) => {
|
|
|
|
|
playground = Some(markdown::Playground {
|
2021-10-28 20:55:02 -07:00
|
|
|
crate_name: Some(krate.name(tcx).to_string()),
|
2021-03-23 11:04:09 -07:00
|
|
|
url: s.to_string(),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
(sym::issue_tracker_base_url, Some(s)) => {
|
|
|
|
|
issue_tracker_base_url = Some(s.to_string());
|
|
|
|
|
}
|
|
|
|
|
(sym::html_no_source, None) if attr.is_word() => {
|
|
|
|
|
include_sources = false;
|
2021-02-13 21:28:34 -08:00
|
|
|
}
|
2021-03-23 11:04:09 -07:00
|
|
|
_ => {}
|
2021-02-13 21:28:34 -08:00
|
|
|
}
|
|
|
|
|
}
|
2021-04-06 00:07:46 +02:00
|
|
|
|
2021-10-31 20:59:22 -07:00
|
|
|
let (local_sources, matches) = collect_spans_and_sources(
|
2021-04-13 15:52:41 +02:00
|
|
|
tcx,
|
2021-10-31 20:59:22 -07:00
|
|
|
&krate,
|
2021-04-13 15:52:41 +02:00
|
|
|
&src_root,
|
|
|
|
|
include_sources,
|
|
|
|
|
generate_link_to_definition,
|
|
|
|
|
);
|
2021-04-06 00:07:46 +02:00
|
|
|
|
2021-02-13 21:28:34 -08:00
|
|
|
let (sender, receiver) = channel();
|
|
|
|
|
let mut scx = SharedContext {
|
|
|
|
|
tcx,
|
|
|
|
|
src_root,
|
2021-04-06 00:07:46 +02:00
|
|
|
local_sources,
|
2021-02-13 21:28:34 -08:00
|
|
|
issue_tracker_base_url,
|
|
|
|
|
layout,
|
|
|
|
|
created_dirs: Default::default(),
|
|
|
|
|
sort_modules_alphabetically,
|
|
|
|
|
style_files,
|
|
|
|
|
resource_suffix,
|
|
|
|
|
static_root_path,
|
|
|
|
|
fs: DocFS::new(sender),
|
|
|
|
|
codes: ErrorCodes::from(unstable_features.is_nightly_build()),
|
|
|
|
|
playground,
|
2021-02-21 14:21:43 -08:00
|
|
|
all: RefCell::new(AllTypes::new()),
|
|
|
|
|
errors: receiver,
|
|
|
|
|
redirections: if generate_redirect_map { Some(Default::default()) } else { None },
|
2021-04-12 19:42:26 -07:00
|
|
|
show_type_layout,
|
2021-04-06 00:07:46 +02:00
|
|
|
span_correspondance_map: matches,
|
2021-08-22 03:36:37 +00:00
|
|
|
cache,
|
2021-09-16 18:12:45 -07:00
|
|
|
call_locations,
|
2021-02-13 21:28:34 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Add the default themes to the `Vec` of stylepaths
|
|
|
|
|
//
|
|
|
|
|
// Note that these must be added before `sources::render` is called
|
|
|
|
|
// so that the resulting source pages are styled
|
|
|
|
|
//
|
|
|
|
|
// `light.css` is not disabled because it is the stylesheet that stays loaded
|
|
|
|
|
// by the browser as the theme stylesheet. The theme system (hackily) works by
|
|
|
|
|
// changing the href to this stylesheet. All other themes are disabled to
|
|
|
|
|
// prevent rule conflicts
|
2021-11-22 23:23:58 -08:00
|
|
|
scx.style_files.push(StylePath { path: PathBuf::from("light.css") });
|
|
|
|
|
scx.style_files.push(StylePath { path: PathBuf::from("dark.css") });
|
|
|
|
|
scx.style_files.push(StylePath { path: PathBuf::from("ayu.css") });
|
2021-02-13 21:28:34 -08:00
|
|
|
|
|
|
|
|
let dst = output;
|
|
|
|
|
scx.ensure_dir(&dst)?;
|
|
|
|
|
|
|
|
|
|
let mut cx = Context {
|
|
|
|
|
current: Vec::new(),
|
|
|
|
|
dst,
|
|
|
|
|
render_redirect_pages: false,
|
2021-03-01 20:06:49 -08:00
|
|
|
id_map: RefCell::new(id_map),
|
2021-10-23 22:18:33 +02:00
|
|
|
deref_id_map: RefCell::new(FxHashMap::default()),
|
2021-02-21 14:25:21 -08:00
|
|
|
shared: Rc::new(scx),
|
2021-04-06 00:07:46 +02:00
|
|
|
include_sources,
|
2021-02-13 21:28:34 -08:00
|
|
|
};
|
|
|
|
|
|
2021-04-06 00:07:46 +02:00
|
|
|
if emit_crate {
|
2021-10-31 20:59:22 -07:00
|
|
|
sources::render(&mut cx, &krate)?;
|
2021-04-06 00:07:46 +02:00
|
|
|
}
|
|
|
|
|
|
2021-12-11 10:13:16 -08:00
|
|
|
if !no_emit_shared {
|
|
|
|
|
// Build our search index
|
|
|
|
|
let index = build_index(&krate, &mut Rc::get_mut(&mut cx.shared).unwrap().cache, tcx);
|
|
|
|
|
|
|
|
|
|
// Write shared runs within a flock; disable thread dispatching of IO temporarily.
|
|
|
|
|
Rc::get_mut(&mut cx.shared).unwrap().fs.set_sync_only(true);
|
|
|
|
|
write_shared(&cx, &krate, index, &md_opts)?;
|
|
|
|
|
Rc::get_mut(&mut cx.shared).unwrap().fs.set_sync_only(false);
|
|
|
|
|
}
|
2021-04-06 00:07:46 +02:00
|
|
|
|
2021-02-13 21:28:34 -08:00
|
|
|
Ok((cx, krate))
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-01 19:12:03 -08:00
|
|
|
fn make_child_renderer(&self) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
current: self.current.clone(),
|
|
|
|
|
dst: self.dst.clone(),
|
|
|
|
|
render_redirect_pages: self.render_redirect_pages,
|
2021-10-23 22:18:33 +02:00
|
|
|
deref_id_map: RefCell::new(FxHashMap::default()),
|
2021-04-03 15:14:23 +02:00
|
|
|
id_map: RefCell::new(IdMap::new()),
|
2021-03-01 19:12:03 -08:00
|
|
|
shared: Rc::clone(&self.shared),
|
2021-04-06 00:07:46 +02:00
|
|
|
include_sources: self.include_sources,
|
2021-03-01 19:12:03 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-22 19:32:24 -04:00
|
|
|
fn after_krate(&mut self) -> Result<(), Error> {
|
2021-04-22 19:27:37 -04:00
|
|
|
let crate_name = self.tcx().crate_name(LOCAL_CRATE);
|
2021-12-15 14:39:23 +11:00
|
|
|
let final_file = self.dst.join(crate_name.as_str()).join("all.html");
|
2021-02-13 21:28:34 -08:00
|
|
|
let settings_file = self.dst.join("settings.html");
|
|
|
|
|
|
|
|
|
|
let mut root_path = self.dst.to_str().expect("invalid path").to_owned();
|
|
|
|
|
if !root_path.ends_with('/') {
|
|
|
|
|
root_path.push('/');
|
|
|
|
|
}
|
|
|
|
|
let mut page = layout::Page {
|
|
|
|
|
title: "List of all items in this crate",
|
|
|
|
|
css_class: "mod",
|
|
|
|
|
root_path: "../",
|
|
|
|
|
static_root_path: self.shared.static_root_path.as_deref(),
|
|
|
|
|
description: "List of all items in this crate",
|
|
|
|
|
keywords: BASIC_KEYWORDS,
|
|
|
|
|
resource_suffix: &self.shared.resource_suffix,
|
|
|
|
|
extra_scripts: &[],
|
|
|
|
|
static_extra_scripts: &[],
|
|
|
|
|
};
|
Simplify and unify rustdoc sidebar styles
This switches to just use size, weight, and spacing to distinguish
headings in the sidebar. We no longer use boxes, horizontal bars, or
centering to distinguish headings. This makes it much easier to
understand the hierarchy of headings, and reduces visual noise.
I also refactored how the mobile topbar works. Previously, we tried to
shift around elements from the sidebar to make the topbar. Now, the
topbar gets its own elements, which can be styled on their own. This
makes styling and reasoning about those elements simpler.
Because the heading font sizes are bigger, increase the sidebar width
slightly.
As a very minor change, removed version from the "All types" page. It's
now only on the crate page.
2022-01-06 19:48:24 -05:00
|
|
|
let sidebar = if self.shared.cache.crate_version.is_some() {
|
|
|
|
|
format!("<h2 class=\"location\">Crate {}</h2>", crate_name)
|
2021-02-13 21:28:34 -08:00
|
|
|
} else {
|
|
|
|
|
String::new()
|
|
|
|
|
};
|
2021-02-21 14:21:43 -08:00
|
|
|
let all = self.shared.all.replace(AllTypes::new());
|
2021-02-13 21:28:34 -08:00
|
|
|
let v = layout::render(
|
|
|
|
|
&self.shared.layout,
|
|
|
|
|
&page,
|
|
|
|
|
sidebar,
|
|
|
|
|
|buf: &mut Buffer| all.print(buf),
|
|
|
|
|
&self.shared.style_files,
|
|
|
|
|
);
|
2021-08-22 02:25:25 +00:00
|
|
|
self.shared.fs.write(final_file, v)?;
|
2021-02-13 21:28:34 -08:00
|
|
|
|
|
|
|
|
// Generating settings page.
|
|
|
|
|
page.title = "Rustdoc settings";
|
|
|
|
|
page.description = "Settings of Rustdoc";
|
|
|
|
|
page.root_path = "./";
|
|
|
|
|
|
2021-07-16 21:58:23 -07:00
|
|
|
let sidebar = "<h2 class=\"location\">Settings</h2><div class=\"sidebar-elems\"></div>";
|
2021-11-22 23:23:58 -08:00
|
|
|
let theme_names: Vec<String> = self
|
|
|
|
|
.shared
|
|
|
|
|
.style_files
|
|
|
|
|
.iter()
|
|
|
|
|
.map(StylePath::basename)
|
|
|
|
|
.collect::<Result<_, Error>>()?;
|
2021-02-13 21:28:34 -08:00
|
|
|
let v = layout::render(
|
|
|
|
|
&self.shared.layout,
|
|
|
|
|
&page,
|
|
|
|
|
sidebar,
|
|
|
|
|
settings(
|
|
|
|
|
self.shared.static_root_path.as_deref().unwrap_or("./"),
|
|
|
|
|
&self.shared.resource_suffix,
|
2021-11-22 23:23:58 -08:00
|
|
|
theme_names,
|
2021-02-13 21:28:34 -08:00
|
|
|
)?,
|
2021-11-22 23:23:58 -08:00
|
|
|
&self.shared.style_files,
|
2021-02-13 21:28:34 -08:00
|
|
|
);
|
2021-08-22 02:25:25 +00:00
|
|
|
self.shared.fs.write(settings_file, v)?;
|
2021-02-21 14:21:43 -08:00
|
|
|
if let Some(ref redirections) = self.shared.redirections {
|
2021-02-13 21:28:34 -08:00
|
|
|
if !redirections.borrow().is_empty() {
|
|
|
|
|
let redirect_map_path =
|
2021-12-15 14:39:23 +11:00
|
|
|
self.dst.join(crate_name.as_str()).join("redirect-map.json");
|
2021-02-13 21:28:34 -08:00
|
|
|
let paths = serde_json::to_string(&*redirections.borrow()).unwrap();
|
2021-12-15 14:39:23 +11:00
|
|
|
self.shared.ensure_dir(&self.dst.join(crate_name.as_str()))?;
|
2021-08-22 02:25:25 +00:00
|
|
|
self.shared.fs.write(redirect_map_path, paths)?;
|
2021-02-13 21:28:34 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Flush pending errors.
|
2021-02-21 14:25:21 -08:00
|
|
|
Rc::get_mut(&mut self.shared).unwrap().fs.close();
|
2021-04-22 19:32:24 -04:00
|
|
|
let nb_errors =
|
|
|
|
|
self.shared.errors.iter().map(|err| self.tcx().sess.struct_err(&err).emit()).count();
|
2021-02-13 21:28:34 -08:00
|
|
|
if nb_errors > 0 {
|
|
|
|
|
Err(Error::new(io::Error::new(io::ErrorKind::Other, "I/O error"), ""))
|
|
|
|
|
} else {
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-22 19:53:38 -04:00
|
|
|
fn mod_item_in(&mut self, item: &clean::Item) -> Result<(), Error> {
|
2021-02-13 21:28:34 -08:00
|
|
|
// Stripped modules survive the rustdoc passes (i.e., `strip-private`)
|
|
|
|
|
// if they contain impls for public types. These modules can also
|
|
|
|
|
// contain items such as publicly re-exported structures.
|
|
|
|
|
//
|
|
|
|
|
// External crates will provide links to these structures, so
|
|
|
|
|
// these modules are recursed into, but not rendered normally
|
|
|
|
|
// (a flag on the context).
|
|
|
|
|
if !self.render_redirect_pages {
|
|
|
|
|
self.render_redirect_pages = item.is_stripped();
|
|
|
|
|
}
|
|
|
|
|
let scx = &self.shared;
|
2021-12-15 06:18:18 +11:00
|
|
|
let item_name = item.name.unwrap();
|
|
|
|
|
self.dst.push(&*item_name.as_str());
|
2021-04-22 19:53:38 -04:00
|
|
|
self.current.push(item_name);
|
2021-02-13 21:28:34 -08:00
|
|
|
|
|
|
|
|
info!("Recursing into {}", self.dst.display());
|
|
|
|
|
|
2021-04-20 19:10:00 -04:00
|
|
|
let buf = self.render_item(item, true);
|
2021-02-13 21:28:34 -08:00
|
|
|
// buf will be empty if the module is stripped and there is no redirect for it
|
|
|
|
|
if !buf.is_empty() {
|
|
|
|
|
self.shared.ensure_dir(&self.dst)?;
|
|
|
|
|
let joint_dst = self.dst.join("index.html");
|
2021-08-22 02:25:25 +00:00
|
|
|
scx.fs.write(joint_dst, buf)?;
|
2021-02-13 21:28:34 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Render sidebar-items.js used throughout this module.
|
|
|
|
|
if !self.render_redirect_pages {
|
|
|
|
|
let module = match *item.kind {
|
|
|
|
|
clean::StrippedItem(box clean::ModuleItem(ref m)) | clean::ModuleItem(ref m) => m,
|
|
|
|
|
_ => unreachable!(),
|
|
|
|
|
};
|
|
|
|
|
let items = self.build_sidebar_items(module);
|
2022-01-10 21:01:09 +01:00
|
|
|
let js_dst = self.dst.join(&format!("sidebar-items{}.js", self.shared.resource_suffix));
|
2021-02-13 21:28:34 -08:00
|
|
|
let v = format!("initSidebarItems({});", serde_json::to_string(&items).unwrap());
|
2021-08-22 02:25:25 +00:00
|
|
|
scx.fs.write(js_dst, v)?;
|
2021-02-13 21:28:34 -08:00
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-22 19:46:53 -04:00
|
|
|
fn mod_item_out(&mut self) -> Result<(), Error> {
|
2021-02-13 21:28:34 -08:00
|
|
|
info!("Recursed; leaving {}", self.dst.display());
|
|
|
|
|
|
|
|
|
|
// Go back to where we were at
|
|
|
|
|
self.dst.pop();
|
|
|
|
|
self.current.pop();
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn item(&mut self, item: clean::Item) -> Result<(), Error> {
|
|
|
|
|
// Stripped modules survive the rustdoc passes (i.e., `strip-private`)
|
|
|
|
|
// if they contain impls for public types. These modules can also
|
|
|
|
|
// contain items such as publicly re-exported structures.
|
|
|
|
|
//
|
|
|
|
|
// External crates will provide links to these structures, so
|
|
|
|
|
// these modules are recursed into, but not rendered normally
|
|
|
|
|
// (a flag on the context).
|
|
|
|
|
if !self.render_redirect_pages {
|
|
|
|
|
self.render_redirect_pages = item.is_stripped();
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-20 19:10:00 -04:00
|
|
|
let buf = self.render_item(&item, false);
|
2021-02-13 21:28:34 -08:00
|
|
|
// buf will be empty if the item is stripped and there is no redirect for it
|
|
|
|
|
if !buf.is_empty() {
|
|
|
|
|
let name = item.name.as_ref().unwrap();
|
|
|
|
|
let item_type = item.type_();
|
2021-12-15 14:39:23 +11:00
|
|
|
let file_name = &item_path(item_type, name.as_str());
|
2021-02-13 21:28:34 -08:00
|
|
|
self.shared.ensure_dir(&self.dst)?;
|
|
|
|
|
let joint_dst = self.dst.join(file_name);
|
2021-08-22 02:25:25 +00:00
|
|
|
self.shared.fs.write(joint_dst, buf)?;
|
2021-02-13 21:28:34 -08:00
|
|
|
|
|
|
|
|
if !self.render_redirect_pages {
|
2021-02-21 14:21:43 -08:00
|
|
|
self.shared.all.borrow_mut().append(full_path(self, &item), &item_type);
|
2021-02-13 21:28:34 -08:00
|
|
|
}
|
|
|
|
|
// If the item is a macro, redirect from the old macro URL (with !)
|
|
|
|
|
// to the new one (without).
|
|
|
|
|
if item_type == ItemType::Macro {
|
|
|
|
|
let redir_name = format!("{}.{}!.html", item_type, name);
|
2021-02-21 14:21:43 -08:00
|
|
|
if let Some(ref redirections) = self.shared.redirections {
|
2021-02-13 21:28:34 -08:00
|
|
|
let crate_name = &self.shared.layout.krate;
|
|
|
|
|
redirections.borrow_mut().insert(
|
|
|
|
|
format!("{}/{}", crate_name, redir_name),
|
|
|
|
|
format!("{}/{}", crate_name, file_name),
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
let v = layout::redirect(file_name);
|
|
|
|
|
let redir_dst = self.dst.join(redir_name);
|
2021-08-22 02:25:25 +00:00
|
|
|
self.shared.fs.write(redir_dst, v)?;
|
2021-02-13 21:28:34 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn cache(&self) -> &Cache {
|
2021-08-22 03:36:37 +00:00
|
|
|
&self.shared.cache
|
2021-02-13 21:28:34 -08:00
|
|
|
}
|
|
|
|
|
}
|
2021-02-13 23:03:07 -08:00
|
|
|
|
|
|
|
|
fn make_item_keywords(it: &clean::Item) -> String {
|
|
|
|
|
format!("{}, {}", BASIC_KEYWORDS, it.name.as_ref().unwrap())
|
|
|
|
|
}
|