use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use maud::Markup;
use maud::html;
use pathdiff::diff_paths;
enum Token<'a> {
Ident(&'a str),
Other(&'a str),
}
fn is_ident_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_'
}
fn tokenize(ty: &str) -> Vec<Token<'_>> {
let mut tokens = Vec::new();
let mut chars = ty.char_indices().peekable();
while let Some(&(start, first)) = chars.peek() {
let ident = is_ident_char(first);
let mut end = start;
while let Some(&(idx, c)) = chars.peek() {
if is_ident_char(c) == ident {
end = idx + c.len_utf8();
chars.next();
} else {
break;
}
}
let run = &ty[start..end];
tokens.push(if ident {
Token::Ident(run)
} else {
Token::Other(run)
});
}
tokens
}
#[derive(Debug, Default)]
pub(crate) struct PageLinkIndex {
targets: HashMap<String, PathBuf>,
}
impl PageLinkIndex {
pub(crate) fn from_pages<I, S>(pages: I) -> Self
where
I: IntoIterator<Item = (S, PathBuf)>,
S: Into<String>,
{
let mut seen: HashMap<String, Option<PathBuf>> = HashMap::new();
for (name, path) in pages {
seen.entry(name.into())
.and_modify(|slot| *slot = None)
.or_insert(Some(path));
}
let targets = seen
.into_iter()
.filter_map(|(name, path)| path.map(|path| (name, path)))
.collect();
Self { targets }
}
fn href(&self, name: &str, page_dir: &Path) -> Option<String> {
let target = self.targets.get(name)?;
let relative = diff_paths(target, page_dir).unwrap_or_else(|| target.clone());
Some(relative.to_string_lossy().replace('\\', "/"))
}
pub(crate) fn render_type(&self, ty: &str, page_dir: &Path) -> Markup {
let tokens = tokenize(ty);
html! {
code {
@for token in &tokens {
@match token {
Token::Ident(name) => {
@if let Some(href) = self.href(name, page_dir) {
a href=(href) { (name) }
} @else {
(name)
}
}
Token::Other(text) => {
(text)
}
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
use std::path::PathBuf;
use super::PageLinkIndex;
#[test]
fn links_unique_struct_and_enum_types() {
let links = PageLinkIndex::from_pages([
("Scroll", PathBuf::from("main/Scroll-struct.html")),
("Spellbook", PathBuf::from("main/Spellbook-enum.html")),
]);
let html = links
.render_type("Array[Scroll]?", Path::new("main"))
.into_string();
assert!(html.contains("href=\"Scroll-struct.html\""));
assert!(html.contains("Array["));
}
#[test]
fn links_enum_types_relative_to_page() {
let links =
PageLinkIndex::from_pages([("Spellbook", PathBuf::from("main/Spellbook-enum.html"))]);
let html = links
.render_type("Spellbook", Path::new("main/nested"))
.into_string();
assert!(html.contains("href=\"../Spellbook-enum.html\""));
}
#[test]
fn unknown_identifiers_stay_plain() {
let links =
PageLinkIndex::from_pages([("Scroll", PathBuf::from("main/Scroll-struct.html"))]);
let html = links
.render_type("Map[String, Int]", Path::new("main"))
.into_string();
assert!(!html.contains("href"));
assert!(html.contains("Map[String, Int]"));
}
#[test]
fn ambiguous_names_stay_plain() {
let links = PageLinkIndex::from_pages([
("Scroll", PathBuf::from("main/Scroll-struct.html")),
("Scroll", PathBuf::from("other/Scroll-struct.html")),
]);
let html = links.render_type("Scroll", Path::new("main")).into_string();
assert!(!html.contains("href"));
assert!(html.contains("<code>"));
assert!(html.contains("Scroll"));
}
}