use crate::*; use vagabond::*; pub struct Website { pub name: String, pub pages: Vec, pub static_files: Vec, pub static_dirs: Vec, } pub struct Page { pub name: String, // Display name pub parent_url: String, // URL base for relative links pub file_url: String, // Safe file name, no extension pub full_url: String, // Safe full URL, no extension pub source_path: PathBuf, // Absolute path to source file pub document: MarkdownDocument, // File content parsed as markdown pub headings: Vec, // Safe name of each document heading } pub struct StaticItem { pub full_url: String, // Safe full URL, with extension pub source_path: PathBuf, // Absolute path to source file } impl Page { pub fn back_string(&self) -> String { let mut back = String::new(); for c in self.full_url.chars() { if c == '/' { back.push_str("../"); } } return back; } } impl Website { pub fn from_path(path: &Path) -> Self { let mut new = Self { pages: Vec::new(), static_files: Vec::new(), static_dirs: Vec::new(), name: match Entry::from_path(path) { Ok(entry) => entry.name, Err(err) => error!("Couldn't open {:?}: {:?}", &path, err), }, }; new.collect_entry(path, path); return new; } fn collect_entry(&mut self, path: &Path, prefix: &Path) { let entry = Entry::from_path(path).unwrap(); // Ignore dotted entries. if entry.name.starts_with('.') { return; } // Get name and extension. let (mut name, extension) = entry.split_name(); if let Some((prefix, suffix)) = name.split_once(' ') { if prefix.chars().all(|c| "0123456789-".contains(c)) { name = suffix.to_string(); } } let file_url = make_url_safe(&name); // Generate parent URL, used only for files. let source_path = entry.original_path.clone(); let relative_path = source_path.strip_prefix(prefix).unwrap_or_else( |_| error!("Path doesn't start with {:?}: {:?}", prefix, source_path)); let mut parent_url = String::new(); let mut components: Vec<_> = relative_path.components().collect(); components.pop(); // Remove file segment. for c in &components { let segment = &make_url_safe(&c.as_os_str().to_string_lossy()); parent_url.push_str(segment); parent_url.push('/') }; // Process each entry. if entry.is_directory() { if let Some(stripped) = entry.name.strip_prefix("!") { let full_url = make_url_safe(stripped); self.static_dirs.push(StaticItem { full_url, source_path }); } else { for child in list_directory(entry.original_path).unwrap() { self.collect_entry(&child.original_path, prefix); } } } else { match extension.as_str() { "md" => { let markdown = std::fs::read_to_string(&source_path).unwrap(); let document = MarkdownDocument::from_str(&markdown); let headings = document.blocks.iter() .filter_map(|block| if let Block::Heading { line, .. } = block { Some(make_url_safe(&line.to_string())) } else { None }).collect(); // Change name and path if this is an index file. let mut name = name; let mut file_url = file_url; let mut full_url = format!("{parent_url}{file_url}"); if file_url == "+index" { if components.is_empty() { // This is the index file for the whole site. name = String::from("Home"); file_url = String::from("index"); full_url = String::from("index"); } else { // This is an index file for a directory. name = components[components.len()-1] .as_os_str().to_string_lossy().to_string(); file_url = make_url_safe(&name); full_url = parent_url.strip_suffix('/').unwrap_or(&parent_url).to_string(); } } self.pages.push( Page { name, parent_url, file_url, full_url, source_path, document, headings }); }, _ => { let full_url = format!("{parent_url}{file_url}.{extension}"); self.static_files.push(StaticItem { full_url, source_path }); }, } } } // Ext is extension without a dot. // Checks if a relative link to an internal page name can be reached from // the current page, and returns a resolved absolute link to the page with extension. pub fn has_page(&self, from: &Page, path: &str, ext: &str) -> Option { // Remove heading fragment and file extension. let (path, heading) = match path.rsplit_once('#') { Some((path, heading)) => match heading.is_empty() { true => (path, None), false => (path, Some(heading)), } None => (path, None), }; let path = path.strip_suffix(&format!(".{ext}")).unwrap_or(path); // Attach parent of current page to given path. let directory = match from.parent_url.rsplit_once('/') { Some((parent, _)) => parent, None => &from.parent_url, }; let full_path = match path.starts_with("/") { true => path.to_string(), false => format!("{directory}/{path}"), }; // Remove relative portions of path. let segments: Vec<&str> = full_path.split("/") .filter(|seg| !seg.is_empty() && *seg != ".") .collect(); let mut reduced_segments: Vec<&str> = segments.windows(2) .filter(|w| w[1] != "..") .map(|w| w[1]) .collect(); // The first segment is always skipped by the previous step. if !segments.is_empty() && segments.get(1) != Some(&"..") { if segments[0] != ".." { reduced_segments.insert(0, segments[0]); } } let path = reduced_segments.join("/"); for page in &self.pages { if page.full_url == path { if let Some(heading) = heading { if !page.headings.contains(&make_url_safe(heading)) { warn!("Page {:?} contains link to nonexistent heading {heading:?} on page {path:?}", from.name); } } return Some(format!("{path}.{ext}")); } } return None; } }