use crate::*;
use vagabond::*;
pub struct Website {
pub name: String,
pub pages: Vec<Page>,
pub static_files: Vec<StaticItem>,
pub static_dirs: Vec<StaticItem>,
}
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<String>, // 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<String> {
// 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;
}
}