diff options
author | Ben Bridle <ben@derelict.engineering> | 2025-01-09 22:15:55 +1300 |
---|---|---|
committer | Ben Bridle <ben@derelict.engineering> | 2025-01-09 22:16:12 +1300 |
commit | ee28abce78ffa3b53ff039ff8640a3b37dc5348b (patch) | |
tree | 2cec62a7930874d1164a1d74e24778a9d4b5c286 /src/collect_files.rs | |
parent | ec6ef10964fd605d7a911fee47bc3cc0a031bdaa (diff) | |
download | toaster-ee28abce78ffa3b53ff039ff8640a3b37dc5348b.zip |
Rewrite link handling and add navigation features to generated HTML
Diffstat (limited to 'src/collect_files.rs')
-rw-r--r-- | src/collect_files.rs | 174 |
1 files changed, 106 insertions, 68 deletions
diff --git a/src/collect_files.rs b/src/collect_files.rs index 88d065f..d9cfb37 100644 --- a/src/collect_files.rs +++ b/src/collect_files.rs @@ -11,13 +11,20 @@ pub struct Website { } 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 name: String, // Display name of this page + pub name_url: String, // URL name for this page, no extension + pub full_url: String, // Full URL for this page, no extension + pub parents: Vec<String>, // Parent directory components, unsafe + pub parent_url: String, // Base URL for links in this page 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 headings: Vec<Heading>, // Ordered list of all headings in page +} + +pub struct Heading { + pub name: String, + pub url: String, + pub level: Level, } pub struct StaticItem { @@ -28,14 +35,12 @@ pub struct StaticItem { 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("../"); - } + pub fn root(&self) -> String { + let mut root = String::new(); + for _ in &self.parents { + root.push_str("../"); } - return back; + return root; } } @@ -59,9 +64,7 @@ impl Website { 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; - } + 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(' ') { @@ -69,18 +72,14 @@ impl Website { name = suffix.to_string(); } } - let file_url = make_url_safe(&name); + let name_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('/') - }; + |_| error!("Path doesn't start with {prefix:?}: {source_path:?}")); + let mut parents: Vec<_> = relative_path.components() + .map(|c| c.as_os_str().to_string_lossy().to_string()).collect(); + parents.pop(); // Remove file segment. // Process each entry. if entry.is_directory() { @@ -98,34 +97,74 @@ impl Website { 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())) + .filter_map(|block| if let Block::Heading { line, level } = block { + let name = line.to_string(); + let url = make_url_safe(&name); + let level = level.to_owned(); + Some(Heading { name, url, level }) } 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() { + if name_url == "+index" { + if parents.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"); + self.pages.push(Page { + name: String::from("Home"), + name_url: String::from("index"), + full_url: String::from("index"), + parents, + parent_url: String::from(""), + source_path, + document, + headings, + }); } 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(); + let name = parents[parents.len()-1].clone(); + let name_url = make_url_safe(&name); + let mut full_url = String::new(); + for parent in &parents { + full_url.push_str(&make_url_safe(parent)); + full_url.push('/'); + } + let parent_url = full_url.clone(); + full_url.pop(); + parents.pop(); + self.pages.push(Page { + name, + name_url, + full_url, + parents, + parent_url, + source_path, + document, + headings, + }); } + } else { + let mut full_url = String::new(); + for parent in &parents { + full_url.push_str(&make_url_safe(parent)); + full_url.push('/'); + } + full_url.push_str(&name_url); + let mut parent_url = full_url.clone(); + parent_url.push('/'); + self.pages.push(Page { + name, name_url, full_url, + parents, parent_url, + source_path, + document, headings, + }); } - self.pages.push( - Page { name, parent_url, file_url, full_url, source_path, document, headings }); }, _ => { - let full_url = format!("{parent_url}{file_url}.{extension}"); + let mut parent_url = String::new(); + for parent in &parents { + parent_url.push_str(&make_url_safe(parent)); + parent_url.push('/'); + } + let full_url = format!("{parent_url}{name_url}.{extension}"); self.static_files.push(StaticItem { full_url, source_path }); }, } @@ -144,42 +183,41 @@ impl Website { } 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}"), - }; + let mut path = path.strip_suffix(&format!(".{ext}")).unwrap_or(path).to_string(); + // Attach parent if not an absolute path. + if !path.starts_with('/') { + path = format!("{}{path}", from.parent_url); + } - // Remove relative portions of path. - let segments: Vec<&str> = full_path.split("/") - .filter(|seg| !seg.is_empty() && *seg != ".") + // Iteratively collapse ".." segments. + let mut segments: Vec<&str> = path.split('/') + .filter(|s| !s.is_empty() && *s != ".") .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]); + 'outer: loop { + for i in 0..(segments.len()-1) { + if segments[i] == ".." { + if i == 0 { + segments.remove(0); + } else { + segments.remove(i-1); + segments.remove(i-1); + } + continue 'outer; + } } + break; } - let path = reduced_segments.join("/"); - + // Find page with this path in website. + let path = 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)) { + if !page.headings.iter().any(|h| h.url == make_url_safe(heading)) { warn!("Page {:?} contains link to nonexistent heading {heading:?} on page {path:?}", from.name); } } - return Some(format!("{path}.{ext}")); + let root = from.root(); + return Some(format!("{root}{path}.{ext}")); } } return None; |