pub const INDEX_STEMS: &[&str] = &["index", "readme", "_index", "main"];
const KNOWN_LANG_SUFFIXES: &[&str] = &[
"en", "zh", "ja", "ko", "de", "fr", "es", "it", "pt", "ru", "ar", "hi", "tr", "pl", "nl", "sv",
"da", "fi", "no", "cs", "hu", "ro", "el", "vi", "th", "id", "he", "uk", "bg", "hr", "sr", "sk",
"sl", "et", "lv", "lt",
"cy", "mi", "bo", "gd", "eu", "ca", "gl", "sw",
"zh-hans", "zh-hant", "zh-cn", "zh-tw", "pt-br", "en-us", "en-gb",
];
pub fn known_language_codes() -> &'static [&'static str] {
KNOWN_LANG_SUFFIXES
}
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 lang_suffix(stem: &str) -> Option<&str> {
let (_, suffix) = stem.rsplit_once('.')?;
if KNOWN_LANG_SUFFIXES.contains(&suffix.to_lowercase().as_str()) {
Some(suffix)
} else {
None
}
}
pub fn is_known_language_code(code: &str) -> bool {
KNOWN_LANG_SUFFIXES.contains(&code.to_lowercase().as_str())
}
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_in_folder_marked<'a>(
filenames: &[&'a str],
folder_name: &str,
marked: &[&str],
) -> Option<&'a str> {
if !marked.is_empty() {
let mut hits: Vec<&'a str> = filenames
.iter()
.copied()
.filter(|f| marked.iter().any(|m| m.eq_ignore_ascii_case(f)))
.collect();
hits.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase()));
if let Some(&f) = hits.first() {
return Some(f);
}
}
detect_home_file_in_folder(filenames, folder_name)
}
pub fn site_name(
homepage_filename: Option<&str>,
homepage_title: Option<&str>,
folder_name: &str,
) -> String {
let folder_label = crate::heading::filename_text(folder_name);
let folder_label = if folder_label.is_empty() {
folder_name.to_string()
} else {
folder_label
};
let title = match homepage_title.map(str::trim).filter(|t| !t.is_empty()) {
Some(t) => t,
None => return folder_label, };
let is_index_home = homepage_filename
.map(|f| {
let stem = std::path::Path::new(f)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or(f);
is_home_file(stem, folder_name)
})
.unwrap_or(false);
if is_index_home && (title == folder_label || is_index_stem(title)) {
return folder_label;
}
title.to_string() }
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_marker_beats_index() {
assert_eq!(
detect_home_file_in_folder_marked(
&["index.md", "home.md", "a.md"],
"anyfolder",
&["home.md"],
),
Some("home.md")
);
}
#[test]
fn test_marker_survives_folder_rename() {
assert_eq!(
detect_home_file_in_folder_marked(&["oldname.md", "b.md"], "newname", &["oldname.md"]),
Some("oldname.md")
);
}
#[test]
fn test_no_marker_matches_filename_detection() {
let files = ["index.md", "z.md"];
assert_eq!(
detect_home_file_in_folder_marked(&files, "f", &[]),
detect_home_file_in_folder(&files, "f"),
);
}
#[test]
fn test_multiple_markers_pick_alphabetically_first() {
assert_eq!(
detect_home_file_in_folder_marked(&["b.md", "a.md"], "f", &["b.md", "a.md"]),
Some("a.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")
);
}
#[test]
fn test_site_name_root_index_no_title_uses_folder() {
assert_eq!(
site_name(Some("index.md"), Some("My Site"), "My Site"),
"My Site"
);
assert_eq!(site_name(Some("index.md"), Some("index"), "My Site"), "My Site");
assert_eq!(site_name(Some("index.md"), None, "My Site"), "My Site");
}
#[test]
fn test_site_name_frontmatter_title_on_index_home_wins() {
assert_eq!(
site_name(Some("index.md"), Some("My Blog"), "site-folder"),
"My Blog"
);
}
#[test]
fn test_site_name_titled_index_not_suppressed() {
assert_eq!(
site_name(Some("about.md"), Some("Index"), "My Site"),
"Index"
);
}
#[test]
fn test_site_name_frontmatter_title_wins_for_article_home() {
assert_eq!(
site_name(Some("about.md"), Some("My Blog"), "folder"),
"My Blog"
);
}
#[test]
fn test_site_name_self_named_home_uses_folder() {
assert_eq!(
site_name(Some("recipes.md"), Some("recipes"), "recipes"),
"recipes"
);
}
#[test]
fn test_site_name_no_homepage_falls_back_to_folder() {
assert_eq!(site_name(None, None, "My Site"), "My Site");
}
#[test]
fn test_site_name_folder_name_normalized() {
assert_eq!(site_name(Some("index.md"), None, "my-site"), "my site");
}
#[test]
fn test_site_name_readme_home_uses_folder() {
assert_eq!(
site_name(Some("README.md"), Some("README"), "Docs"),
"Docs"
);
}
}