2020-07-19 20:23:47 +00:00
|
|
|
// SPDX-FileCopyrightText: 2020 Robin Krahl <robin.krahl@ireas.org>
|
|
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
|
|
|
|
//! Search index for a documentation source.
|
|
|
|
//!
|
|
|
|
//! The search index is read from the `search-index.js` file generated by rustdoc. It contains a
|
|
|
|
//! list of items groupd by their crate.
|
|
|
|
//!
|
|
|
|
//! For details on the format of the search index, see the `html/render.rs` file in `librustdoc`.
|
2020-09-11 19:11:07 +00:00
|
|
|
//! Note that the format of the search index changed in April 2020 (Rust 1.44.0) with commit
|
2020-07-19 20:23:47 +00:00
|
|
|
//! b4fb3069ce82f61f84a9487d17fb96389d55126a. We only support the new format as the old format is
|
|
|
|
//! much harder to parse.
|
2020-07-19 21:45:08 +00:00
|
|
|
//!
|
|
|
|
//! For details on the generation of the search index, see the `html/render/cache.rs` file in
|
|
|
|
//! `librustdoc`.
|
2020-07-19 20:23:47 +00:00
|
|
|
|
|
|
|
use std::collections;
|
|
|
|
use std::fmt;
|
|
|
|
use std::fs;
|
|
|
|
use std::io;
|
|
|
|
use std::path;
|
|
|
|
|
2020-07-21 10:34:46 +00:00
|
|
|
use crate::doc;
|
|
|
|
|
2020-07-19 20:23:47 +00:00
|
|
|
#[derive(Debug)]
|
|
|
|
pub struct Index {
|
2020-07-24 16:10:53 +00:00
|
|
|
path: path::PathBuf,
|
2020-07-19 20:23:47 +00:00
|
|
|
data: Data,
|
|
|
|
}
|
|
|
|
|
2020-07-21 10:34:46 +00:00
|
|
|
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
2020-07-19 20:23:47 +00:00
|
|
|
pub struct IndexItem {
|
2020-07-21 10:34:46 +00:00
|
|
|
pub name: doc::Fqn,
|
2020-08-18 19:26:39 +00:00
|
|
|
pub ty: doc::ItemType,
|
2020-07-19 20:23:47 +00:00
|
|
|
pub description: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl fmt::Display for IndexItem {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
if self.description.is_empty() {
|
2020-08-18 19:26:39 +00:00
|
|
|
write!(f, "{} ({})", &self.name, self.ty.name())
|
2020-07-19 20:23:47 +00:00
|
|
|
} else {
|
2020-08-18 19:43:37 +00:00
|
|
|
write!(
|
|
|
|
f,
|
|
|
|
"{} ({}): {}",
|
|
|
|
&self.name,
|
|
|
|
self.ty.name(),
|
|
|
|
&self.description
|
|
|
|
)
|
2020-07-19 20:23:47 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Default, PartialEq, serde::Deserialize)]
|
|
|
|
#[serde(transparent)]
|
|
|
|
struct Data {
|
|
|
|
crates: collections::HashMap<String, CrateData>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Default, PartialEq, serde::Deserialize)]
|
|
|
|
struct CrateData {
|
|
|
|
#[serde(rename = "i")]
|
|
|
|
items: Vec<ItemData>,
|
2020-07-19 22:36:54 +00:00
|
|
|
#[serde(rename = "p")]
|
|
|
|
paths: Vec<(usize, String)>,
|
2020-07-19 20:23:47 +00:00
|
|
|
}
|
|
|
|
|
2020-07-22 14:21:27 +00:00
|
|
|
#[derive(Debug, PartialEq, serde_tuple::Deserialize_tuple)]
|
2020-07-19 20:23:47 +00:00
|
|
|
struct ItemData {
|
2020-08-17 11:42:10 +00:00
|
|
|
#[serde(deserialize_with = "deserialize_item_type")]
|
2020-07-22 14:21:27 +00:00
|
|
|
ty: doc::ItemType,
|
2020-07-19 20:23:47 +00:00
|
|
|
name: String,
|
|
|
|
path: String,
|
|
|
|
desc: String,
|
|
|
|
parent: Option<usize>,
|
|
|
|
_ignored: serde_json::Value,
|
|
|
|
}
|
|
|
|
|
2020-08-17 11:42:10 +00:00
|
|
|
fn deserialize_item_type<'de, D>(d: D) -> Result<doc::ItemType, D::Error>
|
|
|
|
where
|
|
|
|
D: serde::de::Deserializer<'de>,
|
|
|
|
{
|
|
|
|
use doc::ItemType;
|
|
|
|
use serde::de::{Deserialize, Error};
|
|
|
|
|
|
|
|
match u8::deserialize(d)? {
|
|
|
|
0 => Ok(ItemType::Module),
|
|
|
|
1 => Ok(ItemType::ExternCrate),
|
|
|
|
2 => Ok(ItemType::Import),
|
|
|
|
3 => Ok(ItemType::Struct),
|
|
|
|
4 => Ok(ItemType::Enum),
|
|
|
|
5 => Ok(ItemType::Function),
|
|
|
|
6 => Ok(ItemType::Typedef),
|
|
|
|
7 => Ok(ItemType::Static),
|
|
|
|
8 => Ok(ItemType::Trait),
|
|
|
|
9 => Ok(ItemType::Impl),
|
|
|
|
10 => Ok(ItemType::TyMethod),
|
|
|
|
11 => Ok(ItemType::Method),
|
|
|
|
12 => Ok(ItemType::StructField),
|
|
|
|
13 => Ok(ItemType::Variant),
|
|
|
|
14 => Ok(ItemType::Macro),
|
|
|
|
15 => Ok(ItemType::Primitive),
|
|
|
|
16 => Ok(ItemType::AssocType),
|
|
|
|
17 => Ok(ItemType::Constant),
|
|
|
|
18 => Ok(ItemType::AssocConst),
|
|
|
|
19 => Ok(ItemType::Union),
|
|
|
|
20 => Ok(ItemType::ForeignType),
|
|
|
|
21 => Ok(ItemType::Keyword),
|
|
|
|
22 => Ok(ItemType::OpaqueTy),
|
|
|
|
23 => Ok(ItemType::ProcAttribute),
|
|
|
|
24 => Ok(ItemType::ProcDerive),
|
|
|
|
25 => Ok(ItemType::TraitAlias),
|
|
|
|
_ => Err(D::Error::custom("Unexpected item type")),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-19 20:23:47 +00:00
|
|
|
impl Index {
|
|
|
|
pub fn load(path: impl AsRef<path::Path>) -> anyhow::Result<Option<Self>> {
|
|
|
|
use std::io::BufRead;
|
|
|
|
|
|
|
|
anyhow::ensure!(
|
|
|
|
path.as_ref().is_file(),
|
|
|
|
"Search index '{}' must be a file",
|
|
|
|
path.as_ref().display()
|
|
|
|
);
|
|
|
|
|
|
|
|
let mut json: Option<String> = None;
|
|
|
|
let mut finished = false;
|
|
|
|
|
2020-07-24 16:10:53 +00:00
|
|
|
for line in io::BufReader::new(fs::File::open(path.as_ref())?).lines() {
|
2020-07-19 20:23:47 +00:00
|
|
|
let line = line?;
|
|
|
|
if let Some(json) = &mut json {
|
|
|
|
if line == "}');" {
|
|
|
|
json.push_str("}");
|
|
|
|
finished = true;
|
|
|
|
break;
|
|
|
|
} else {
|
|
|
|
json.push_str(line.trim_end_matches('\\'));
|
|
|
|
}
|
|
|
|
} else if line == "var searchIndex = JSON.parse('{\\" {
|
|
|
|
json = Some(String::from("{"));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(json) = json {
|
|
|
|
if finished {
|
|
|
|
use anyhow::Context;
|
|
|
|
let json = json.replace("\\'", "'");
|
|
|
|
let data: Data =
|
|
|
|
serde_json::from_str(&json).context("Could not parse search index")?;
|
|
|
|
|
2020-07-24 16:10:53 +00:00
|
|
|
Ok(Some(Index {
|
|
|
|
data,
|
|
|
|
path: path.as_ref().to_owned(),
|
|
|
|
}))
|
2020-07-19 20:23:47 +00:00
|
|
|
} else {
|
2020-07-24 16:10:53 +00:00
|
|
|
log::info!(
|
|
|
|
"Did not find JSON end line in search index '{}'",
|
|
|
|
path.as_ref().display()
|
|
|
|
);
|
2020-07-19 20:23:47 +00:00
|
|
|
Ok(None)
|
|
|
|
}
|
|
|
|
} else {
|
2020-07-24 16:10:53 +00:00
|
|
|
log::info!(
|
|
|
|
"Did not find JSON start line in search index '{}'",
|
|
|
|
path.as_ref().display()
|
|
|
|
);
|
2020-07-19 20:23:47 +00:00
|
|
|
Ok(None)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-21 10:34:46 +00:00
|
|
|
pub fn find(&self, name: &doc::Name) -> Vec<IndexItem> {
|
2020-07-24 16:10:53 +00:00
|
|
|
log::info!(
|
|
|
|
"Looking up '{}' in search index '{}'",
|
|
|
|
name,
|
|
|
|
self.path.display()
|
|
|
|
);
|
2020-07-19 20:23:47 +00:00
|
|
|
let mut matches: Vec<IndexItem> = Vec::new();
|
|
|
|
for (krate, data) in &self.data.crates {
|
2020-07-19 21:45:08 +00:00
|
|
|
let mut path = krate;
|
2020-07-19 20:23:47 +00:00
|
|
|
for item in &data.items {
|
2020-07-19 21:45:08 +00:00
|
|
|
path = if item.path.is_empty() {
|
|
|
|
path
|
2020-07-19 20:44:21 +00:00
|
|
|
} else {
|
|
|
|
&item.path
|
|
|
|
};
|
2020-07-20 08:44:37 +00:00
|
|
|
|
2020-07-22 14:21:27 +00:00
|
|
|
if item.ty == doc::ItemType::AssocType {
|
2020-07-20 08:44:37 +00:00
|
|
|
continue;
|
|
|
|
}
|
2020-07-20 09:48:29 +00:00
|
|
|
|
2020-07-19 22:36:54 +00:00
|
|
|
let full_path = match item.parent {
|
|
|
|
Some(idx) => {
|
|
|
|
let parent = &data.paths[idx].1;
|
|
|
|
format!("{}::{}", path, parent)
|
|
|
|
}
|
|
|
|
None => path.to_owned(),
|
|
|
|
};
|
2020-07-21 10:34:46 +00:00
|
|
|
let full_name: doc::Fqn = format!("{}::{}", &full_path, &item.name).into();
|
|
|
|
if full_name.ends_with(&name) {
|
2020-07-24 16:10:53 +00:00
|
|
|
log::info!("Found index match '{}'", full_name);
|
2020-07-19 20:23:47 +00:00
|
|
|
matches.push(IndexItem {
|
2020-07-21 10:34:46 +00:00
|
|
|
name: full_name,
|
2020-08-18 19:26:39 +00:00
|
|
|
ty: item.ty,
|
2020-07-19 20:23:47 +00:00
|
|
|
description: item.desc.clone(),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
matches.sort_unstable();
|
|
|
|
matches.dedup();
|
|
|
|
matches
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
2020-09-11 19:11:07 +00:00
|
|
|
use super::{CrateData, Data, Index, IndexItem, ItemData};
|
2020-07-22 14:21:27 +00:00
|
|
|
use crate::doc::ItemType;
|
2020-09-11 19:11:07 +00:00
|
|
|
use crate::test_utils::with_rustdoc;
|
2020-07-19 20:23:47 +00:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_empty() {
|
|
|
|
let expected: Data = Default::default();
|
|
|
|
let actual: Data = serde_json::from_str("{}").unwrap();
|
|
|
|
assert_eq!(expected, actual);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_empty_crate() {
|
|
|
|
let mut expected: Data = Default::default();
|
|
|
|
expected
|
|
|
|
.crates
|
|
|
|
.insert("test".to_owned(), Default::default());
|
2020-07-19 22:36:54 +00:00
|
|
|
let actual: Data = serde_json::from_str("{\"test\": {\"i\": [], \"p\": []}}").unwrap();
|
2020-07-19 20:23:47 +00:00
|
|
|
assert_eq!(expected, actual);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_one_item() {
|
|
|
|
let mut expected: Data = Default::default();
|
|
|
|
let mut krate: CrateData = Default::default();
|
2020-07-22 14:21:27 +00:00
|
|
|
krate.items.push(ItemData {
|
|
|
|
ty: ItemType::Module,
|
|
|
|
name: "name".to_owned(),
|
|
|
|
path: "path".to_owned(),
|
|
|
|
desc: "desc".to_owned(),
|
|
|
|
parent: None,
|
|
|
|
_ignored: Default::default(),
|
|
|
|
});
|
2020-07-19 20:23:47 +00:00
|
|
|
expected.crates.insert("test".to_owned(), krate);
|
|
|
|
let actual: Data = serde_json::from_str(
|
2020-07-19 22:36:54 +00:00
|
|
|
"{\"test\": {\"i\": [[0, \"name\", \"path\", \"desc\", null, null]], \"p\": []}}",
|
2020-07-19 20:23:47 +00:00
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(expected, actual);
|
|
|
|
}
|
2020-09-11 19:11:07 +00:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_index() {
|
|
|
|
with_rustdoc(">=1.44.0", |_, path| {
|
|
|
|
let index = Index::load(path.join("search-index.js")).unwrap().unwrap();
|
|
|
|
|
|
|
|
let empty: Vec<IndexItem> = Vec::new();
|
|
|
|
|
|
|
|
let node_data_ref = vec![IndexItem {
|
|
|
|
name: "kuchiki::NodeDataRef".to_owned().into(),
|
|
|
|
ty: ItemType::Struct,
|
|
|
|
description: "Holds a strong reference to a node, but dereferences to…".to_owned(),
|
|
|
|
}];
|
|
|
|
assert_eq!(node_data_ref, index.find(&"NodeDataRef".to_owned().into()));
|
|
|
|
assert_eq!(
|
|
|
|
node_data_ref,
|
|
|
|
index.find(&"kuchiki::NodeDataRef".to_owned().into())
|
|
|
|
);
|
|
|
|
assert_eq!(empty, index.find(&"DataRef".to_owned().into()));
|
|
|
|
assert_eq!(empty, index.find(&"NodeDataReff".to_owned().into()));
|
|
|
|
|
|
|
|
let as_node = vec![IndexItem {
|
|
|
|
name: "kuchiki::NodeDataRef::as_node".to_owned().into(),
|
|
|
|
ty: ItemType::Method,
|
|
|
|
description: "Access the corresponding node.".to_owned(),
|
|
|
|
}];
|
|
|
|
assert_eq!(as_node, index.find(&"as_node".to_owned().into()));
|
|
|
|
assert_eq!(
|
|
|
|
as_node,
|
|
|
|
index.find(&"NodeDataRef::as_node".to_owned().into())
|
|
|
|
);
|
|
|
|
assert_eq!(
|
|
|
|
as_node,
|
|
|
|
index.find(&"kuchiki::NodeDataRef::as_node".to_owned().into())
|
|
|
|
);
|
|
|
|
assert_eq!(empty, index.find(&"DataRef::as_node".to_owned().into()));
|
|
|
|
});
|
|
|
|
}
|
2020-07-19 20:23:47 +00:00
|
|
|
}
|