diff options
author | Ben Bridle <bridle.benjamin@gmail.com> | 2025-01-18 12:04:07 +1300 |
---|---|---|
committer | Ben Bridle <bridle.benjamin@gmail.com> | 2025-01-18 12:04:07 +1300 |
commit | 3765c30ef322c31456aa419e970955330a1461ad (patch) | |
tree | f241e990b8ea3eaae01b89647956ab68d63b7499 | |
parent | 9717476273e05379ada099946307ae2682b8abef (diff) | |
download | toaster-3765c30ef322c31456aa419e970955330a1461ad.zip |
Check that links to static files are valid
When a link doesn't contain a protocol, it's assumed to be a link to
a relative or absolute path to an internal static file. This link is
checked against the list of collected static files to ensure that it
exists.
For an unlabelled link to an internal static file, the label will be
the file name without preceding path segments.
-rw-r--r-- | src/collect_files.rs | 56 | ||||
-rw-r--r-- | src/generate_html.rs | 24 |
2 files changed, 53 insertions, 27 deletions
diff --git a/src/collect_files.rs b/src/collect_files.rs index 64cfc30..9aa4983 100644 --- a/src/collect_files.rs +++ b/src/collect_files.rs @@ -273,27 +273,9 @@ impl Website { if !path.starts_with('/') { path = format!("{}{path}", from.parent_url()); } + let path = make_url_safe(&collapse_path(&path)); - // Iteratively collapse ".." segments. - let mut segments: Vec<&str> = path.split('/') - .filter(|s| !s.is_empty() && *s != ".") - .collect(); - 'outer: loop { - for i in 0..(segments.len().saturating_sub(1)) { - if segments[i] == ".." { - if i == 0 { - segments.remove(0); - } else { - segments.remove(i-1); - segments.remove(i-1); - } - continue 'outer; - } - } - break; - } // Find page with this path in website. - let path = make_url_safe(&segments.join("/")); for page in &self.pages { if page.full_url == path { let root = from.root(); @@ -310,6 +292,21 @@ impl Website { return None; } + pub fn has_static(&self, from: &impl LinkFrom, path: &str) -> Option<String> { + // Attach parent if not an absolute path. + let path = match !path.starts_with('/') { + true => collapse_path(&format!("{}{path}", make_url_safe(from.parent_url()))), + false => collapse_path(path), + }; + for file in &self.static_files { + if file.full_url == path { + let root = from.root(); + return Some(format!("{root}{path}")); + } + } + return None; + } + pub fn has_image(&self, file_name: &str) -> bool { let image_path = format!("images/thumb/{file_name}"); self.static_files.iter().any(|s| s.full_url == image_path) @@ -320,3 +317,24 @@ impl Website { } } + +fn collapse_path(path: &str) -> String { + // Iteratively collapse ".." segments. + let mut segments: Vec<&str> = path.split('/') + .filter(|s| !s.is_empty() && *s != ".") + .collect(); + 'outer: loop { + for i in 0..(segments.len().saturating_sub(1)) { + if segments[i] == ".." { + if i == 0 { + segments.remove(0); + } else { + segments.remove(i-1); + segments.remove(i-1); + } + continue 'outer; + } + } + return segments.join("/"); + } +} diff --git a/src/generate_html.rs b/src/generate_html.rs index 6220015..9533410 100644 --- a/src/generate_html.rs +++ b/src/generate_html.rs @@ -289,21 +289,29 @@ fn line_to_html(line: &Line, page: &Page, website: &Website) -> String { html.push_str(&format!("<a href='{path}' class='{class}'>{label}</a>")) } Token::ExternalLink { label, path } => { + let mut path = path.to_owned(); let mut label = label.to_string(); - // Strip the protocol from the path when using the path as a label. - if label.is_empty() { - label = path.to_string(); + + if !path.contains("://") { + // Check that the linked static file exists. + match website.has_static(page, &path) { + Some(resolved_path) => path = resolved_path, + None => warn!("Page {:?} contains link to nonexistent static file {path:?}", page.name), + } + // Take the file name as the label if the link is unlabeled. + if label.is_empty() { + label = match path.rsplit_once('/') { + Some((_, file)) => file.to_string(), + None => path.clone(), + }; + } + } else if label.is_empty() { for protocol in ["mailto://", "http://", "https://"] { if let Some(stripped) = path.strip_prefix(protocol) { label = stripped.to_string(); } } } - // Support absolute local paths. - let path = match path.strip_prefix('/') { - Some(stripped) => format!("{}{stripped}", page.root()), - None => path.to_string(), - }; let label = sanitize_text(&label); html.push_str(&format!("<a href='{path}' class='external'>{label}</a>")); } |