//! Utilities for processing HTML.
use std::collections::HashMap;
use std::fmt::Write;
use std::path::{Component, Path, PathBuf};
/// Utility function to normalize path elements like `..`.
pub(crate) fn normalize_path(path: &Path) -> PathBuf {
let mut components = path.components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
components.next();
PathBuf::from(c.as_os_str())
} else {
PathBuf::new()
};
for component in components {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {
ret.push(Component::RootDir);
}
Component::CurDir => {}
Component::ParentDir => {
if ret.ends_with(Component::ParentDir) {
ret.push(Component::ParentDir);
} else {
let popped = ret.pop();
if !popped && !ret.has_root() {
ret.push(Component::ParentDir);
}
}
}
Component::Normal(c) => {
ret.push(c);
}
}
}
ret
}
/// Helper trait for converting a [`Path`] to a string suitable for an HTML path.
pub(crate) trait ToUrlPath {
fn to_url_path(&self) -> String;
}
impl ToUrlPath for Path {
fn to_url_path(&self) -> String {
// We're generally assuming that all paths we deal with are utf-8.
// The replace here is to handle Windows paths.
self.to_str().unwrap().replace('\\', "/")
}
}
/// Make sure an HTML id is unique.
///
/// The `id_counter` map is used to ensure the ID is globally unique. If the
/// same id appears more than once, then it will have a number added to make
/// it unique.
pub(crate) fn unique_id(id: &str, id_counter: &mut HashMap) -> String {
let mut id = id.to_string();
let id_count = id_counter.entry(id.to_string()).or_insert(0);
if *id_count != 0 {
// FIXME: This should be a loop to ensure that the new ID is also unique.
write!(id, "-{id_count}").unwrap();
}
*id_count += 1;
id
}
/// Generates an HTML id from the given text.
pub(crate) fn id_from_content(content: &str) -> String {
content
.trim()
.chars()
.filter_map(|ch| {
if ch.is_alphanumeric() || ch == '_' || ch == '-' {
Some(ch.to_ascii_lowercase())
} else if ch.is_whitespace() {
Some('-')
} else {
None
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_generates_unique_ids() {
let mut id_counter = Default::default();
assert_eq!(unique_id("", &mut id_counter), "");
assert_eq!(unique_id("Über", &mut id_counter), "Über");
assert_eq!(unique_id("Über", &mut id_counter), "Über-1");
assert_eq!(unique_id("Über", &mut id_counter), "Über-2");
}
#[test]
fn it_normalizes_ids() {
assert_eq!(
id_from_content("`--passes`: add more rustdoc passes"),
"--passes-add-more-rustdoc-passes"
);
assert_eq!(
id_from_content("Method-call 🐙 expressions \u{1f47c}"),
"method-call--expressions-"
);
assert_eq!(id_from_content("_-_12345"), "_-_12345");
assert_eq!(id_from_content("12345"), "12345");
assert_eq!(id_from_content("中文"), "中文");
assert_eq!(id_from_content("にほんご"), "にほんご");
assert_eq!(id_from_content("한국어"), "한국어");
assert_eq!(id_from_content(""), "");
assert_eq!(id_from_content("中文標題 CJK title"), "中文標題-cjk-title");
assert_eq!(id_from_content("Über"), "Über");
}
}