Previously Markdown documentation was not rendered to HTML for search results, which led to the output not being very readable, particularly for inline code. This PR fixes that by rendering Markdown to HTML with the help of pulldown-cmark (the library rustdoc uses to parse Markdown for the main text of documentation). However, the text for the title attribute (the text shown when you hover over an element) still uses the plain-text rendering since it is displayed in browsers as plain-text. Only these styles will be rendered; everything else is stripped away: * *italics* * **bold** * `inline code`
233 lines
7.8 KiB
Rust
233 lines
7.8 KiB
Rust
use std::collections::BTreeMap;
|
|
use std::path::Path;
|
|
|
|
use rustc_data_structures::fx::FxHashMap;
|
|
use rustc_span::symbol::sym;
|
|
use serde::Serialize;
|
|
|
|
use crate::clean::types::GetDefId;
|
|
use crate::clean::{self, AttributesExt};
|
|
use crate::formats::cache::Cache;
|
|
use crate::formats::item_type::ItemType;
|
|
use crate::html::markdown::short_markdown_summary;
|
|
use crate::html::render::{Generic, IndexItem, IndexItemFunctionType, RenderType, TypeWithKind};
|
|
|
|
/// Indicates where an external crate can be found.
|
|
crate enum ExternalLocation {
|
|
/// Remote URL root of the external crate
|
|
Remote(String),
|
|
/// This external crate can be found in the local doc/ folder
|
|
Local,
|
|
/// The external crate could not be found.
|
|
Unknown,
|
|
}
|
|
|
|
/// Attempts to find where an external crate is located, given that we're
|
|
/// rendering in to the specified source destination.
|
|
crate fn extern_location(
|
|
e: &clean::ExternalCrate,
|
|
extern_url: Option<&str>,
|
|
dst: &Path,
|
|
) -> ExternalLocation {
|
|
use ExternalLocation::*;
|
|
// See if there's documentation generated into the local directory
|
|
let local_location = dst.join(&e.name);
|
|
if local_location.is_dir() {
|
|
return Local;
|
|
}
|
|
|
|
if let Some(url) = extern_url {
|
|
let mut url = url.to_string();
|
|
if !url.ends_with('/') {
|
|
url.push('/');
|
|
}
|
|
return Remote(url);
|
|
}
|
|
|
|
// Failing that, see if there's an attribute specifying where to find this
|
|
// external crate
|
|
e.attrs
|
|
.lists(sym::doc)
|
|
.filter(|a| a.has_name(sym::html_root_url))
|
|
.filter_map(|a| a.value_str())
|
|
.map(|url| {
|
|
let mut url = url.to_string();
|
|
if !url.ends_with('/') {
|
|
url.push('/')
|
|
}
|
|
Remote(url)
|
|
})
|
|
.next()
|
|
.unwrap_or(Unknown) // Well, at least we tried.
|
|
}
|
|
|
|
/// Builds the search index from the collected metadata
|
|
crate fn build_index(krate: &clean::Crate, cache: &mut Cache) -> String {
|
|
let mut defid_to_pathid = FxHashMap::default();
|
|
let mut crate_items = Vec::with_capacity(cache.search_index.len());
|
|
let mut crate_paths = vec![];
|
|
|
|
let Cache { ref mut search_index, ref orphan_impl_items, ref paths, ref mut aliases, .. } =
|
|
*cache;
|
|
|
|
// Attach all orphan items to the type's definition if the type
|
|
// has since been learned.
|
|
for &(did, ref item) in orphan_impl_items {
|
|
if let Some(&(ref fqp, _)) = paths.get(&did) {
|
|
search_index.push(IndexItem {
|
|
ty: item.type_(),
|
|
name: item.name.clone().unwrap(),
|
|
path: fqp[..fqp.len() - 1].join("::"),
|
|
desc: item.doc_value().map_or_else(|| String::new(), short_markdown_summary),
|
|
parent: Some(did),
|
|
parent_idx: None,
|
|
search_type: get_index_search_type(&item),
|
|
});
|
|
for alias in item.attrs.get_doc_aliases() {
|
|
aliases
|
|
.entry(alias.to_lowercase())
|
|
.or_insert(Vec::new())
|
|
.push(search_index.len() - 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Reduce `DefId` in paths into smaller sequential numbers,
|
|
// and prune the paths that do not appear in the index.
|
|
let mut lastpath = String::new();
|
|
let mut lastpathid = 0usize;
|
|
|
|
for item in search_index {
|
|
item.parent_idx = item.parent.and_then(|defid| {
|
|
if defid_to_pathid.contains_key(&defid) {
|
|
defid_to_pathid.get(&defid).copied()
|
|
} else {
|
|
let pathid = lastpathid;
|
|
defid_to_pathid.insert(defid, pathid);
|
|
lastpathid += 1;
|
|
|
|
if let Some(&(ref fqp, short)) = paths.get(&defid) {
|
|
crate_paths.push((short, fqp.last().unwrap().clone()));
|
|
Some(pathid)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
});
|
|
|
|
// Omit the parent path if it is same to that of the prior item.
|
|
if lastpath == item.path {
|
|
item.path.clear();
|
|
} else {
|
|
lastpath = item.path.clone();
|
|
}
|
|
crate_items.push(&*item);
|
|
}
|
|
|
|
let crate_doc = krate
|
|
.module
|
|
.as_ref()
|
|
.map(|module| module.doc_value().map_or_else(|| String::new(), short_markdown_summary))
|
|
.unwrap_or_default();
|
|
|
|
#[derive(Serialize)]
|
|
struct CrateData<'a> {
|
|
doc: String,
|
|
#[serde(rename = "i")]
|
|
items: Vec<&'a IndexItem>,
|
|
#[serde(rename = "p")]
|
|
paths: Vec<(ItemType, String)>,
|
|
// The String is alias name and the vec is the list of the elements with this alias.
|
|
//
|
|
// To be noted: the `usize` elements are indexes to `items`.
|
|
#[serde(rename = "a")]
|
|
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
|
|
aliases: &'a BTreeMap<String, Vec<usize>>,
|
|
}
|
|
|
|
// Collect the index into a string
|
|
format!(
|
|
r#""{}":{}"#,
|
|
krate.name,
|
|
serde_json::to_string(&CrateData {
|
|
doc: crate_doc,
|
|
items: crate_items,
|
|
paths: crate_paths,
|
|
aliases,
|
|
})
|
|
.expect("failed serde conversion")
|
|
// All these `replace` calls are because we have to go through JS string for JSON content.
|
|
.replace(r"\", r"\\")
|
|
.replace("'", r"\'")
|
|
// We need to escape double quotes for the JSON.
|
|
.replace("\\\"", "\\\\\"")
|
|
)
|
|
}
|
|
|
|
crate fn get_index_search_type(item: &clean::Item) -> Option<IndexItemFunctionType> {
|
|
let (all_types, ret_types) = match item.kind {
|
|
clean::FunctionItem(ref f) => (&f.all_types, &f.ret_types),
|
|
clean::MethodItem(ref m, _) => (&m.all_types, &m.ret_types),
|
|
clean::TyMethodItem(ref m) => (&m.all_types, &m.ret_types),
|
|
_ => return None,
|
|
};
|
|
|
|
let inputs = all_types
|
|
.iter()
|
|
.map(|(ty, kind)| TypeWithKind::from((get_index_type(&ty), *kind)))
|
|
.filter(|a| a.ty.name.is_some())
|
|
.collect();
|
|
let output = ret_types
|
|
.iter()
|
|
.map(|(ty, kind)| TypeWithKind::from((get_index_type(&ty), *kind)))
|
|
.filter(|a| a.ty.name.is_some())
|
|
.collect::<Vec<_>>();
|
|
let output = if output.is_empty() { None } else { Some(output) };
|
|
|
|
Some(IndexItemFunctionType { inputs, output })
|
|
}
|
|
|
|
fn get_index_type(clean_type: &clean::Type) -> RenderType {
|
|
RenderType {
|
|
ty: clean_type.def_id(),
|
|
idx: None,
|
|
name: get_index_type_name(clean_type, true).map(|s| s.to_ascii_lowercase()),
|
|
generics: get_generics(clean_type),
|
|
}
|
|
}
|
|
|
|
fn get_index_type_name(clean_type: &clean::Type, accept_generic: bool) -> Option<String> {
|
|
match *clean_type {
|
|
clean::ResolvedPath { ref path, .. } => {
|
|
let segments = &path.segments;
|
|
let path_segment = segments.iter().last().unwrap_or_else(|| {
|
|
panic!(
|
|
"get_index_type_name(clean_type: {:?}, accept_generic: {:?}) had length zero path",
|
|
clean_type, accept_generic
|
|
)
|
|
});
|
|
Some(path_segment.name.clone())
|
|
}
|
|
clean::Generic(ref s) if accept_generic => Some(s.clone()),
|
|
clean::Primitive(ref p) => Some(format!("{:?}", p)),
|
|
clean::BorrowedRef { ref type_, .. } => get_index_type_name(type_, accept_generic),
|
|
// FIXME: add all from clean::Type.
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn get_generics(clean_type: &clean::Type) -> Option<Vec<Generic>> {
|
|
clean_type.generics().and_then(|types| {
|
|
let r = types
|
|
.iter()
|
|
.filter_map(|t| {
|
|
get_index_type_name(t, false).map(|name| Generic {
|
|
name: name.to_ascii_lowercase(),
|
|
defid: t.def_id(),
|
|
idx: None,
|
|
})
|
|
})
|
|
.collect::<Vec<_>>();
|
|
if r.is_empty() { None } else { Some(r) }
|
|
})
|
|
}
|