#[must_use]
pub fn derive_page_url(base_url: &str, relative_output_path: &str) -> String {
let base = base_url.trim_end_matches('/');
let normalised = relative_output_path.replace('\\', "/");
let mut rel = normalised.as_str();
loop {
if let Some(stripped) = rel.strip_prefix("./") {
rel = stripped;
} else if let Some(stripped) = rel.strip_prefix('/') {
rel = stripped;
} else {
break;
}
}
if rel.is_empty() || rel == "index.html" {
return format!("{base}/");
}
if let Some(dir) = rel.strip_suffix("/index.html") {
return format!("{base}/{dir}/");
}
format!("{base}/{rel}")
}
#[must_use]
pub fn derive_output_rel_path(content_rel_source_path: &str) -> String {
let normalised = content_rel_source_path.replace('\\', "/");
let rel = normalised.trim_start_matches("./").trim_start_matches('/');
let stripped = rel.strip_suffix(".md").unwrap_or(rel);
if stripped == "index" {
return "index.html".to_string();
}
if let Some(dir) = stripped.strip_suffix("/index") {
return format!("{dir}/index.html");
}
format!("{stripped}/index.html")
}
#[must_use]
pub fn derive_permalink(
base_url: &str,
content_rel_source_path: &str,
) -> String {
derive_page_url(base_url, &derive_output_rel_path(content_rel_source_path))
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn root_index_becomes_bare_base_with_trailing_slash() {
assert_eq!(
derive_page_url("https://example.com", "index.html"),
"https://example.com/"
);
}
#[test]
fn trailing_slash_base_url_does_not_double_slash() {
assert_eq!(
derive_page_url("https://example.com/", "foo/index.html"),
"https://example.com/foo/"
);
}
#[test]
fn nested_index_html_collapses_to_directory_url() {
assert_eq!(
derive_page_url("https://example.com", "posts/bar/index.html"),
"https://example.com/posts/bar/"
);
}
#[test]
fn non_index_output_keeps_file_name() {
assert_eq!(
derive_page_url("https://example.com", "rss.xml"),
"https://example.com/rss.xml"
);
}
#[test]
fn windows_separators_are_normalised() {
assert_eq!(
derive_page_url("https://example.com", "posts\\bar\\index.html"),
"https://example.com/posts/bar/"
);
}
#[test]
fn leading_dot_slash_and_slash_are_ignored() {
assert_eq!(
derive_page_url("https://example.com", "./a/index.html"),
"https://example.com/a/"
);
assert_eq!(
derive_page_url("https://example.com", "/a/index.html"),
"https://example.com/a/"
);
}
#[test]
fn percent_encoding_is_passed_through() {
assert_eq!(
derive_page_url("https://example.com", "caf%C3%A9/index.html"),
"https://example.com/caf%C3%A9/"
);
}
#[test]
fn empty_rel_path_yields_base_with_trailing_slash() {
assert_eq!(
derive_page_url("https://example.com", ""),
"https://example.com/"
);
}
#[test]
fn output_rel_path_agrees_with_compiler_convention() {
assert_eq!(
derive_output_rel_path("posts/foo.md"),
"posts/foo/index.html"
);
assert_eq!(derive_output_rel_path("index.md"), "index.html");
assert_eq!(
derive_output_rel_path("about/index.md"),
"about/index.html"
);
}
#[test]
fn markdown_extension_is_kept_when_not_md() {
assert_eq!(
derive_output_rel_path("foo.markdown"),
"foo.markdown/index.html"
);
}
#[test]
fn windows_source_separator_is_normalised_in_output_path() {
assert_eq!(
derive_output_rel_path("posts\\foo.md"),
"posts/foo/index.html"
);
}
#[test]
fn permalink_composes_both_derivations() {
assert_eq!(
derive_permalink("https://example.com/", "posts/foo.md"),
"https://example.com/posts/foo/"
);
assert_eq!(
derive_permalink("https://example.com", "index.md"),
"https://example.com/"
);
assert_eq!(
derive_permalink("https://example.com", "about/index.md"),
"https://example.com/about/"
);
}
}