pub const INDEX_STEMS: &[&str] = &["index", "readme", "_index", "main"];
const KNOWN_LANG_SUFFIXES: &[&str] = &[
"en", "zh", "zh-hans", "zh-hant", "zh-cn", "zh-tw", "ja", "ko", "fr", "de", "es", "pt", "ru",
"ar",
];
pub fn lang_tree_prefix(path: &str) -> Option<&str> {
let first = path.split('/').next()?;
if first.len() == path.len() {
return None;
}
if KNOWN_LANG_SUFFIXES.contains(&first.to_lowercase().as_str()) {
Some(first)
} else {
None
}
}
pub fn strip_lang_suffix(stem: &str) -> Option<&str> {
let (head, suffix) = stem.rsplit_once('.')?;
if KNOWN_LANG_SUFFIXES.contains(&suffix.to_lowercase().as_str()) {
Some(head)
} else {
None
}
}
pub fn is_index_stem(stem: &str) -> bool {
INDEX_STEMS.contains(&stem.to_lowercase().as_str())
}
pub fn is_home_file(stem: &str, parent_folder_name: &str) -> bool {
if is_index_stem(stem) {
return true;
}
if let Some(bare) = strip_lang_suffix(stem) {
if is_index_stem(bare) {
return true;
}
}
!parent_folder_name.is_empty() && stem.to_lowercase() == parent_folder_name.to_lowercase()
}
pub fn detect_home_file_in_folder<'a>(
filenames: &[&'a str],
folder_name: &str,
) -> Option<&'a str> {
for stem in INDEX_STEMS {
let target_md = format!("{}.md", stem);
if let Some(&f) = filenames.iter().find(|f| f.to_lowercase() == target_md) {
return Some(f);
}
}
for stem in INDEX_STEMS {
if let Some(&f) = filenames.iter().find(|f| {
let lower = f.to_lowercase();
if let Some(name_without_ext) = lower.strip_suffix(".md") {
if let Some(bare) = strip_lang_suffix(name_without_ext) {
return bare == *stem;
}
}
false
}) {
return Some(f);
}
}
for ext in &["pages", "docx"] {
let target = format!("index.{}", ext);
if let Some(&f) = filenames.iter().find(|f| f.to_lowercase() == target) {
return Some(f);
}
}
let self_named = format!("{}.md", folder_name.to_lowercase());
if let Some(&f) = filenames.iter().find(|f| f.to_lowercase() == self_named) {
return Some(f);
}
let mut doc_files: Vec<&&str> = filenames
.iter()
.filter(|f| {
let lower = f.to_lowercase();
lower.ends_with(".md")
|| lower.ends_with(".pages")
|| lower.ends_with(".docx")
|| lower.ends_with(".doc")
})
.collect();
doc_files.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase()));
doc_files.first().map(|f| **f)
}
pub fn detect_home_file<'a>(filenames: &[&'a str]) -> Option<&'a str> {
detect_home_file_in_folder(filenames, "")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_index_stem_all_recognized() {
assert!(is_index_stem("index"));
assert!(is_index_stem("readme"));
assert!(is_index_stem("_index"));
assert!(is_index_stem("main"));
}
#[test]
fn test_is_index_stem_case_insensitive() {
assert!(is_index_stem("INDEX"));
assert!(is_index_stem("README"));
assert!(is_index_stem("Readme"));
assert!(is_index_stem("MAIN"));
assert!(is_index_stem("_Index"));
}
#[test]
fn test_is_index_stem_rejects_non_stems() {
assert!(!is_index_stem("about"));
assert!(!is_index_stem("home"));
assert!(!is_index_stem(""));
}
#[test]
fn test_detect_home_index_md_wins() {
let files = vec!["README.md", "index.md", "main.md"];
assert_eq!(detect_home_file(&files), Some("index.md"));
}
#[test]
fn test_detect_home_readme_over_underscore_index() {
let files = vec!["_index.md", "README.md"];
assert_eq!(detect_home_file(&files), Some("README.md"));
}
#[test]
fn test_detect_home_underscore_index_over_main() {
let files = vec!["main.md", "_index.md"];
assert_eq!(detect_home_file(&files), Some("_index.md"));
}
#[test]
fn test_detect_home_case_insensitive() {
let files = vec!["INDEX.MD"];
assert_eq!(detect_home_file(&files), Some("INDEX.MD"));
}
#[test]
fn test_detect_home_index_pages() {
let files = vec!["index.pages"];
assert_eq!(detect_home_file(&files), Some("index.pages"));
}
#[test]
fn test_detect_home_md_over_pages() {
let files = vec!["index.pages", "readme.md"];
assert_eq!(detect_home_file(&files), Some("readme.md"));
}
#[test]
fn test_detect_home_no_candidates() {
let files = vec!["photo.jpg", "style.css"];
assert_eq!(detect_home_file(&files), None);
}
#[test]
fn test_detect_home_fallback_to_first_doc_alphabetically() {
let files = vec!["zebra.md", "about.md"];
assert_eq!(detect_home_file(&files), Some("about.md"));
}
#[test]
fn test_detect_home_empty_list() {
let files: Vec<&str> = vec![];
assert_eq!(detect_home_file(&files), None);
}
#[test]
fn test_detect_self_named_folder_note() {
let files = vec!["recipes.md", "pasta.md"];
assert_eq!(
detect_home_file_in_folder(&files, "recipes"),
Some("recipes.md")
);
}
#[test]
fn test_index_beats_self_named() {
let files = vec!["recipes.md", "index.md"];
assert_eq!(
detect_home_file_in_folder(&files, "recipes"),
Some("index.md")
);
}
#[test]
fn test_self_named_case_insensitive() {
let files = vec!["Recipes.md"];
assert_eq!(
detect_home_file_in_folder(&files, "recipes"),
Some("Recipes.md")
);
}
#[test]
fn test_readme_beats_self_named() {
let files = vec!["recipes.md", "readme.md"];
assert_eq!(
detect_home_file_in_folder(&files, "recipes"),
Some("readme.md")
);
}
#[test]
fn test_self_named_beats_alphabetical_fallback() {
let files = vec!["about.md", "recipes.md"];
assert_eq!(
detect_home_file_in_folder(&files, "recipes"),
Some("recipes.md")
);
}
#[test]
fn test_is_home_file_index_stem() {
assert!(is_home_file("index", "anything"));
assert!(is_home_file("readme", "anything"));
}
#[test]
fn test_is_home_file_self_named() {
assert!(is_home_file("recipes", "recipes"));
assert!(is_home_file("刘果", "刘果"));
}
#[test]
fn test_is_home_file_self_named_case_insensitive() {
assert!(is_home_file("Recipes", "recipes"));
assert!(is_home_file("recipes", "Recipes"));
}
#[test]
fn test_is_home_file_no_match() {
assert!(!is_home_file("about", "recipes"));
assert!(!is_home_file("about", ""));
}
#[test]
fn test_is_home_file_empty_folder_name() {
assert!(is_home_file("index", ""));
assert!(!is_home_file("about", ""));
}
#[test]
fn test_in_folder_delegates_to_detect_home_file_for_index_stems() {
let files = vec!["index.md", "other.md"];
assert_eq!(
detect_home_file_in_folder(&files, "myfolder"),
Some("index.md")
);
}
#[test]
fn test_is_home_file_with_zh_hans_suffix() {
assert!(is_home_file("index.zh-hans", "anything"));
}
#[test]
fn test_is_home_file_with_en_suffix() {
assert!(is_home_file("index.en", "anything"));
}
#[test]
fn test_is_home_file_with_zh_hant_suffix() {
assert!(is_home_file("readme.zh-hant", "anything"));
}
#[test]
fn test_is_home_file_non_language_suffix_rejected() {
assert!(!is_home_file("index.v2", "anything"));
}
#[test]
fn test_detect_home_bare_stem_wins_over_lang_suffix() {
let files = vec!["index.md", "index.zh-hans.md"];
assert_eq!(
detect_home_file_in_folder(&files, "root"),
Some("index.md")
);
}
#[test]
fn test_detect_home_lang_suffix_recognized_when_no_bare() {
let files = vec!["index.zh-hans.md", "about.md"];
assert_eq!(
detect_home_file_in_folder(&files, "root"),
Some("index.zh-hans.md")
);
}
#[test]
fn test_strip_lang_suffix_zh_shorthand_accepted() {
assert_eq!(strip_lang_suffix("about.zh"), Some("about"));
}
#[test]
fn test_strip_lang_suffix_zh_hans_still_works() {
assert_eq!(strip_lang_suffix("about.zh-hans"), Some("about"));
}
#[test]
fn test_strip_lang_suffix_zh_hant_still_works() {
assert_eq!(strip_lang_suffix("about.zh-hant"), Some("about"));
}
#[test]
fn test_strip_lang_suffix_zh_tw_still_works() {
assert_eq!(strip_lang_suffix("about.zh-tw"), Some("about"));
}
#[test]
fn test_is_home_file_with_zh_shorthand_suffix() {
assert!(is_home_file("index.zh", "anything"));
}
#[test]
fn test_detect_home_zh_shorthand_recognized() {
let files = vec!["index.zh.md", "about.md"];
assert_eq!(
detect_home_file_in_folder(&files, "root"),
Some("index.zh.md")
);
}
}