From 7d097ebe7292d41b8395a6187ac19857f1557f84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Ml=C3=A1dek?= Date: Sun, 25 Oct 2020 15:56:07 +0100 Subject: [PATCH] add backlinks --- src/main.rs | 165 ++++++++++++++++++++++++++++++++------------ templates/main.html | 11 +++ 2 files changed, 132 insertions(+), 44 deletions(-) diff --git a/src/main.rs b/src/main.rs index 5997693..14164b1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,8 +6,8 @@ use chrono::{DateTime, Local}; use clap::{App as ClapApp, Arg}; use linkify::LinkFinder; use log::{info, trace}; -use percent_encoding::utf8_percent_encode; -use pulldown_cmark::{html, Options, Parser}; +use percent_encoding::{percent_decode_str, utf8_percent_encode}; +use pulldown_cmark::{html, Event, Options, Parser, Tag}; use regex::{Captures, Regex}; use std::collections::HashMap; use std::fs::File; @@ -33,7 +33,7 @@ struct MutableState { #[derive(Clone, Debug)] struct GardenCache { - pages: HashMap, + pages: HashMap, files: Vec, } @@ -167,20 +167,41 @@ async fn render( let full_path = data.garden_dir.join(path.as_str()); // Redirect to ".md" version if requested path matches a .md file without the extension - if !full_path.exists() && full_path.extension().is_none() { - let md_path = format!("{}.md", path.to_string()); - if Path::new(&md_path).exists() { - return Ok(HttpResponse::Found() - .header(http::header::LOCATION, md_path) - .finish()); - } + if !full_path.exists() + && full_path.extension().is_none() + && Path::new(&format!("{}.md", full_path.to_str().unwrap())).exists() + { + return Ok(HttpResponse::Found() + .header(http::header::LOCATION, format!("{}.md", path.to_string())) + .finish()); } if full_path.exists() && !path.ends_with(".md") { return Ok(NamedFile::open(full_path)?.into_response(&request)?); } - let page = cache.pages.get(&full_path); + let filename = full_path + .components() + .last() + .unwrap() + .as_os_str() + .to_str() + .unwrap(); + + let normalized_name = normalize_name(filename); + + let mut backlinks: Vec = vec![]; + for (path, page) in cache.pages.iter() { + if page + .links + .iter() + .any(|link| normalize_name(link.as_str()) == normalized_name) + { + backlinks.push(normalize_name(path)); + } + } + + let page = cache.pages.get(path.as_ref()); let mut context = Context::new(); context.insert("version", VERSION); @@ -193,14 +214,7 @@ async fn render( "page_title", &match page { Some(page) => page.title.clone(), - None => full_path - .components() - .last() - .unwrap() - .as_os_str() - .to_str() - .unwrap() - .to_string(), + None => filename.to_string(), }, ); context.insert( @@ -213,6 +227,7 @@ async fn render( .map_err(ErrorInternalServerError)?, }, ); + context.insert("backlinks", &backlinks); context.insert( "mtime", &match page { @@ -265,12 +280,12 @@ fn update_garden>( let markdown_paths = files .iter() - .filter(|p| p.to_str().unwrap_or("").ends_with(".md")) - .map(|p| garden_path.join(p)); + .filter(|p| p.to_str().unwrap_or("").ends_with(".md")); for path in markdown_paths { trace!("Loading {} into cache...", path.display()); - let mtime = path.metadata().unwrap().modified().ok(); - if let Some(page) = pages.get(&path) { + let full_path = garden_path.join(path); + let mtime = full_path.metadata().unwrap().modified().ok(); + if let Some(page) = pages.get(path.to_str().unwrap()) { if let (Some(fs_time), Some(last_time)) = (mtime, page.timestamp) { if fs_time == last_time { continue; @@ -278,34 +293,29 @@ fn update_garden>( } } - let mut file = File::open(&path)?; + let mut file = File::open(&full_path)?; let mut file_string = String::new(); file.read_to_string(&mut file_string)?; let markdown_source = preprocess_markdown(file_string); - let parser = Parser::new_ext(markdown_source.as_str(), Options::all()); - let mut html_output = String::new(); - html::push_html(&mut html_output, parser); - - // TODO! - let h1_regex = Regex::new(r"

([^>]+)

").unwrap(); - let title = match h1_regex.captures(&html_output) { - Some(h1_match) => h1_match.get(1).unwrap().as_str(), - _ => &path - .components() - .last() - .unwrap() - .as_os_str() - .to_str() - .unwrap_or("???"), - }; + let result = GardenParser::parse(&markdown_source); pages.insert( - path.clone(), + String::from(path.to_str().unwrap()), ParsedPage { timestamp: mtime, - html: html_output.clone(), - title: String::from(title), - links: vec![], // todo!, + html: result.html, + links: result.links, + title: match result.title { + Some(title) => title, + _ => String::from( + path.components() + .last() + .unwrap() + .as_os_str() + .to_str() + .unwrap_or("???"), + ), + }, }, ); } @@ -315,6 +325,67 @@ fn update_garden>( Ok(result) } +struct GardenParser<'a> { + parser: Parser<'a>, + last_nontext_event: Option>, + current_top_heading: u32, + top_heading_text: &'a mut Option, + links: &'a mut Vec, +} + +struct ParseResult { + html: String, + title: Option, + links: Vec, +} + +impl<'a> GardenParser<'a> { + fn parse>(text: &'a S) -> ParseResult { + let mut title: Option = None; + let mut links: Vec = vec![]; + + let parser = GardenParser { + parser: Parser::new_ext(text.as_ref(), Options::all()), + last_nontext_event: None, + current_top_heading: 999, + top_heading_text: &mut title, + links: &mut links, + }; + + let mut html = String::new(); + html::push_html(&mut html, parser); + + ParseResult { html, title, links } + } +} + +impl<'a> Iterator for GardenParser<'a> { + type Item = Event<'a>; + + fn next(&mut self) -> Option { + let event = self.parser.next(); + + if let Some(event) = &event { + if let Event::Start(Tag::Link(_, str, _)) = &event { + self.links.push(str.to_string()); + } + + if let Some(Event::Start(Tag::Heading(hl))) = self.last_nontext_event { + if hl < self.current_top_heading { + self.current_top_heading = hl; + if let Event::Text(str) = &event { + *self.top_heading_text = Some(str.clone().into_string()); + } + } + } + + self.last_nontext_event = Some(event.clone()); + } + + event + } +} + fn preprocess_markdown(string: String) -> String { let double_brackets = Regex::new(r"\[\[(?P[\w .]+)\]\]").unwrap(); let finder = LinkFinder::new(); @@ -351,3 +422,9 @@ fn preprocess_markdown(string: String) -> String { result_string } + +fn normalize_name(filename: &str) -> String { + let decoded = percent_decode_str(filename).decode_utf8_lossy(); + let result = decoded.strip_suffix(".md"); + String::from(result.unwrap_or(filename)) +} diff --git a/templates/main.html b/templates/main.html index b695214..ecf9414 100644 --- a/templates/main.html +++ b/templates/main.html @@ -28,6 +28,17 @@
{{content | safe}} +
+ + {% if backlinks %} +

Linked from:

+
    + {% for link in backlinks %} +
  • {{link}}
  • + {% endfor %} +
+ {% endif %} + {% if mtime %}