use std::collections::{BTreeMap, HashMap};
use serde::Deserialize;
use serde_repr::Deserialize_repr;
use crate::error::{Error, Result};
#[cfg(feature = "index-v1")]
mod v1;
#[cfg(feature = "index-v2")]
mod v2;
#[cfg_attr(test, derive(Clone, Copy, Eq, PartialEq, serde::Serialize))]
enum Version {
#[cfg(feature = "index-v1")]
V1,
#[cfg(feature = "index-v2")]
V2,
V3,
}
impl Version {
fn detect(index: &str) -> Option<Self> {
#[cfg(feature = "index-v1")]
if index.starts_with(r#"var N=null,E="",T="t",U="u",searchIndex={};"#) {
return Some(Self::V1);
}
#[cfg(feature = "index-v2")]
if index.ends_with(r#"addSearchOptions(searchIndex);initSearch(searchIndex);"#) {
return Some(Self::V2);
}
if index.ends_with(r#"if (window.initSearch) {window.initSearch(searchIndex)};"#)
|| index.trim_end().ends_with(
r#"if (typeof exports !== 'undefined') {exports.searchIndex = searchIndex};"#,
)
{
Some(Self::V3)
} else {
None
}
}
}
#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
struct IndexData {
crates: HashMap<String, CrateData>,
}
#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
struct CrateData {
#[allow(dead_code)]
doc: String,
items: Vec<IndexItem>,
paths: Vec<(ItemType, String)>,
}
#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
struct IndexItem {
ty: ItemType,
name: String,
path: String,
#[allow(dead_code)]
desc: String,
parent_idx: Option<usize>,
}
#[derive(Clone, Copy, Debug, Deserialize_repr)]
#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
#[repr(u8)]
enum ItemType {
Module = 0,
ExternCrate = 1,
Import = 2,
Struct = 3,
Enum = 4,
Function = 5,
Typedef = 6,
Static = 7,
Trait = 8,
Impl = 9,
TyMethod = 10,
Method = 11,
StructField = 12,
Variant = 13,
Macro = 14,
Primitive = 15,
AssocType = 16,
Constant = 17,
AssocConst = 18,
Union = 19,
ForeignType = 20,
Keyword = 21,
OpaqueTy = 22,
ProcAttribute = 23,
ProcDerive = 24,
TraitAlias = 25,
}
impl ItemType {
const fn as_str(self) -> &'static str {
match self {
Self::Module => "mod",
Self::ExternCrate => "externcrate",
Self::Import => "import",
Self::Struct => "struct",
Self::Union => "union",
Self::Enum => "enum",
Self::Function => "fn",
Self::Typedef => "type",
Self::Static => "static",
Self::Trait => "trait",
Self::Impl => "impl",
Self::TyMethod => "tymethod",
Self::Method => "method",
Self::StructField => "structfield",
Self::Variant => "variant",
Self::Macro => "macro",
Self::Primitive => "primitive",
Self::AssocType => "associatedtype",
Self::Constant => "constant",
Self::AssocConst => "associatedconstant",
Self::ForeignType => "foreigntype",
Self::Keyword => "keyword",
Self::OpaqueTy => "opaque",
Self::ProcAttribute => "attr",
Self::ProcDerive => "derive",
Self::TraitAlias => "traitalias",
}
}
}
#[derive(Debug, Deserialize)]
#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
struct RawIndexData {
#[serde(flatten)]
crates: HashMap<String, RawCrateData>,
}
#[derive(Debug, Deserialize)]
#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
struct RawCrateData {
doc: String,
t: Vec<ItemType>,
n: Vec<String>,
q: Vec<String>,
d: Vec<String>,
i: Vec<usize>,
p: Vec<(ItemType, String)>,
}
pub fn load(index: &str) -> Result<HashMap<String, BTreeMap<String, String>>> {
let raw = match Version::detect(index) {
Some(Version::V3) => load_raw(index)?,
#[cfg(feature = "index-v2")]
Some(Version::V2) => v2::load_raw(index)?,
#[cfg(feature = "index-v1")]
Some(Version::V1) => v1::load_raw(index)?,
None => return Err(Error::UnsupportedIndexVersion),
};
Ok(generate_mapping(transform(raw)))
}
fn load_raw(index: &str) -> Result<RawIndexData> {
let json = {
let mut json = index
.lines()
.filter_map(|l| {
if l.starts_with('"') {
l.strip_suffix('\\')
} else {
None
}
})
.fold(String::from("{"), |mut json, l| {
json.push_str(l);
json
});
json.push('}');
json.replace("\\\\\"", "\\\"")
.replace(r"\'", "'")
.replace(r"\\", r"\")
};
serde_json::from_str(&json).map_err(Into::into)
}
fn transform(raw: RawIndexData) -> IndexData {
IndexData {
crates: raw
.crates
.into_iter()
.map(|(name, raw_data)| {
let length = raw_data.t.len();
let (items, _) = raw_data
.t
.into_iter()
.zip(raw_data.n.into_iter())
.zip(raw_data.q.into_iter())
.zip(raw_data.d.into_iter())
.zip(raw_data.i.into_iter())
.fold(
(Vec::with_capacity(length), String::new()),
|(mut items, path), ((((t, n), q), d), i)| {
let path = if q.is_empty() { path } else { q };
items.push(IndexItem {
ty: t,
name: n,
path: path.clone(),
desc: d,
parent_idx: if i > 0 { Some(i - 1) } else { None },
});
(items, path)
},
);
(
name,
CrateData {
doc: raw_data.doc,
items,
paths: raw_data.p,
},
)
})
.collect(),
}
}
fn generate_mapping(data: IndexData) -> HashMap<String, BTreeMap<String, String>> {
data.crates
.into_iter()
.map(|(name, data)| (name, generate_crate_mapping(data)))
.collect()
}
fn generate_crate_mapping(data: CrateData) -> BTreeMap<String, String> {
let paths = data.paths;
data.items
.into_iter()
.map(|item| {
let full_path = if let Some(idx) = item.parent_idx {
format!("{}::{}::{}", item.path, paths[idx].1, item.name)
} else {
format!("{}::{}", item.path, item.name)
};
let url = if let Some(parent) = item.parent_idx.map(|i| &paths[i]) {
format!(
"{}/{}.{}.html#{}.{}",
item.path.replace("::", "/"),
parent.0.as_str(),
parent.1,
item.ty.as_str(),
item.name
)
} else {
format!(
"{}/{}.{}.html",
item.path.replace("::", "/"),
item.ty.as_str(),
item.name
)
};
(full_path, url)
})
.collect()
}
#[cfg(test)]
mod tests {
use std::fs;
use insta::glob;
use super::*;
#[test]
fn test_version_detect() {
glob!("fixtures/*.js", |path| {
let input = fs::read_to_string(path).unwrap();
let data = Version::detect(&input);
insta::assert_yaml_snapshot!(data);
});
}
#[allow(clippy::bind_instead_of_map)]
#[test]
fn test_load_raw() {
glob!("fixtures/*.js", |path| {
let input = fs::read_to_string(path).unwrap();
let data = Version::detect(&input).and_then(|v| match v {
#[cfg(feature = "index-v1")]
Version::V1 => Some(v1::load_raw(&input).unwrap()),
#[cfg(feature = "index-v2")]
Version::V2 => Some(v2::load_raw(&input).unwrap()),
Version::V3 => Some(load_raw(&input).unwrap()),
});
insta::assert_yaml_snapshot!(data);
});
}
#[allow(clippy::bind_instead_of_map)]
#[test]
fn test_transform() {
glob!("fixtures/*.js", |path| {
let input = fs::read_to_string(path).unwrap();
let data = Version::detect(&input)
.and_then(|v| match v {
#[cfg(feature = "index-v1")]
Version::V1 => Some(v1::load_raw(&input).unwrap()),
#[cfg(feature = "index-v2")]
Version::V2 => Some(v2::load_raw(&input).unwrap()),
Version::V3 => Some(load_raw(&input).unwrap()),
})
.map(transform);
insta::assert_yaml_snapshot!(data);
});
}
#[allow(clippy::bind_instead_of_map)]
#[test]
fn test_generate_mapping() {
glob!("fixtures/*.js", |path| {
let input = fs::read_to_string(path).unwrap();
let data = Version::detect(&input)
.and_then(|v| match v {
#[cfg(feature = "index-v1")]
Version::V1 => Some(v1::load_raw(&input).unwrap()),
#[cfg(feature = "index-v2")]
Version::V2 => Some(v2::load_raw(&input).unwrap()),
Version::V3 => Some(load_raw(&input).unwrap()),
})
.map(transform)
.map(generate_mapping);
insta::assert_yaml_snapshot!(data);
});
}
}