use std::path::Path;
use super::registry::DocId;
const REPO_BLOB_BASE: &str = "https://github.com/mijowi/edamame/blob/main";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DocLinkResolution {
Doc(DocId, Option<String>),
External(String),
}
pub fn resolve_doc_reference(path: &Path, fragment: Option<String>) -> DocLinkResolution {
if let Some(id) = path.to_str().and_then(DocId::from_slug) {
return DocLinkResolution::Doc(id, fragment);
}
let mut url = format!("{REPO_BLOB_BASE}/{}", repo_relative_path(path));
if let Some(f) = fragment {
url.push('#');
url.push_str(&f);
}
DocLinkResolution::External(url)
}
fn repo_relative_path(path: &Path) -> String {
let raw = path.to_string_lossy().replace('\\', "/");
let mut parts: Vec<&str> = Vec::new();
parts.push("docs");
for segment in raw.split('/') {
match segment {
"" | "." => {}
".." => {
parts.pop();
}
other => parts.push(other),
}
}
parts.join("/")
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn resolve(p: &str, frag: Option<&str>) -> DocLinkResolution {
resolve_doc_reference(&PathBuf::from(p), frag.map(str::to_owned))
}
#[test]
fn a_sibling_page_resolves_to_that_page() {
assert_eq!(
resolve("security.md", None),
DocLinkResolution::Doc(DocId::Security, None)
);
}
#[test]
fn a_fragment_is_carried_through_to_the_page() {
assert_eq!(
resolve("keybindings.md", Some("terminal-compatibility")),
DocLinkResolution::Doc(
DocId::Keybindings,
Some("terminal-compatibility".to_owned())
)
);
}
#[test]
fn a_contributor_page_becomes_a_github_url_under_docs() {
assert_eq!(
resolve("dev/theming.md", None),
DocLinkResolution::External(format!("{REPO_BLOB_BASE}/docs/dev/theming.md"))
);
}
#[test]
fn a_parent_reference_climbs_out_of_the_docs_directory() {
assert_eq!(
resolve("../SECURITY.md", None),
DocLinkResolution::External(format!("{REPO_BLOB_BASE}/SECURITY.md"))
);
}
#[test]
fn an_external_link_keeps_its_fragment() {
assert_eq!(
resolve("dev/security-invariants.md", Some("checklist")),
DocLinkResolution::External(format!(
"{REPO_BLOB_BASE}/docs/dev/security-invariants.md#checklist"
))
);
}
#[test]
fn an_unknown_file_name_does_not_masquerade_as_a_page() {
assert_eq!(
resolve("nonexistent.md", None),
DocLinkResolution::External(format!("{REPO_BLOB_BASE}/docs/nonexistent.md"))
);
}
#[test]
fn every_cross_link_in_the_shipped_docs_resolves_somewhere_sane() {
for page in super::super::registry::ALL_DOCS {
for target in md_link_targets(page.source) {
let (path, _) = match target.split_once('#') {
Some((p, f)) => (p, Some(f)),
None => (target.as_str(), None),
};
if path.is_empty() {
continue; }
let resolved = resolve(path, None);
if let DocLinkResolution::External(url) = &resolved {
assert!(
url.contains("/docs/dev/") || url.ends_with("/SECURITY.md"),
"{} links to {path}, which resolves to an unexpected {url}",
page.slug
);
}
}
}
}
fn md_link_targets(src: &str) -> Vec<String> {
let mut out = Vec::new();
let bytes: Vec<char> = src.chars().collect();
let mut i = 0;
while i + 1 < bytes.len() {
if bytes[i] == ']' && bytes[i + 1] == '(' {
let start = i + 2;
let mut j = start;
while j < bytes.len() && bytes[j] != ')' {
j += 1;
}
if j < bytes.len() {
let target: String = bytes[start..j].iter().collect();
if target.contains(".md") && !target.starts_with("http") {
out.push(target);
}
}
i = j;
}
i += 1;
}
out
}
}