add backlinks

This commit is contained in:
Tomáš Mládek 2020-10-25 15:56:07 +01:00
parent 9d8f308a4d
commit 7d097ebe72
2 changed files with 132 additions and 44 deletions

View file

@ -6,8 +6,8 @@ use chrono::{DateTime, Local};
use clap::{App as ClapApp, Arg}; use clap::{App as ClapApp, Arg};
use linkify::LinkFinder; use linkify::LinkFinder;
use log::{info, trace}; use log::{info, trace};
use percent_encoding::utf8_percent_encode; use percent_encoding::{percent_decode_str, utf8_percent_encode};
use pulldown_cmark::{html, Options, Parser}; use pulldown_cmark::{html, Event, Options, Parser, Tag};
use regex::{Captures, Regex}; use regex::{Captures, Regex};
use std::collections::HashMap; use std::collections::HashMap;
use std::fs::File; use std::fs::File;
@ -33,7 +33,7 @@ struct MutableState {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
struct GardenCache { struct GardenCache {
pages: HashMap<PathBuf, ParsedPage>, pages: HashMap<String, ParsedPage>,
files: Vec<PathBuf>, files: Vec<PathBuf>,
} }
@ -167,20 +167,41 @@ async fn render(
let full_path = data.garden_dir.join(path.as_str()); let full_path = data.garden_dir.join(path.as_str());
// Redirect to ".md" version if requested path matches a .md file without the extension // Redirect to ".md" version if requested path matches a .md file without the extension
if !full_path.exists() && full_path.extension().is_none() { if !full_path.exists()
let md_path = format!("{}.md", path.to_string()); && full_path.extension().is_none()
if Path::new(&md_path).exists() { && Path::new(&format!("{}.md", full_path.to_str().unwrap())).exists()
return Ok(HttpResponse::Found() {
.header(http::header::LOCATION, md_path) return Ok(HttpResponse::Found()
.finish()); .header(http::header::LOCATION, format!("{}.md", path.to_string()))
} .finish());
} }
if full_path.exists() && !path.ends_with(".md") { if full_path.exists() && !path.ends_with(".md") {
return Ok(NamedFile::open(full_path)?.into_response(&request)?); 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<String> = 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(); let mut context = Context::new();
context.insert("version", VERSION); context.insert("version", VERSION);
@ -193,14 +214,7 @@ async fn render(
"page_title", "page_title",
&match page { &match page {
Some(page) => page.title.clone(), Some(page) => page.title.clone(),
None => full_path None => filename.to_string(),
.components()
.last()
.unwrap()
.as_os_str()
.to_str()
.unwrap()
.to_string(),
}, },
); );
context.insert( context.insert(
@ -213,6 +227,7 @@ async fn render(
.map_err(ErrorInternalServerError)?, .map_err(ErrorInternalServerError)?,
}, },
); );
context.insert("backlinks", &backlinks);
context.insert( context.insert(
"mtime", "mtime",
&match page { &match page {
@ -265,12 +280,12 @@ fn update_garden<P: AsRef<Path>>(
let markdown_paths = files let markdown_paths = files
.iter() .iter()
.filter(|p| p.to_str().unwrap_or("").ends_with(".md")) .filter(|p| p.to_str().unwrap_or("").ends_with(".md"));
.map(|p| garden_path.join(p));
for path in markdown_paths { for path in markdown_paths {
trace!("Loading {} into cache...", path.display()); trace!("Loading {} into cache...", path.display());
let mtime = path.metadata().unwrap().modified().ok(); let full_path = garden_path.join(path);
if let Some(page) = pages.get(&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 let (Some(fs_time), Some(last_time)) = (mtime, page.timestamp) {
if fs_time == last_time { if fs_time == last_time {
continue; continue;
@ -278,34 +293,29 @@ fn update_garden<P: AsRef<Path>>(
} }
} }
let mut file = File::open(&path)?; let mut file = File::open(&full_path)?;
let mut file_string = String::new(); let mut file_string = String::new();
file.read_to_string(&mut file_string)?; file.read_to_string(&mut file_string)?;
let markdown_source = preprocess_markdown(file_string); let markdown_source = preprocess_markdown(file_string);
let parser = Parser::new_ext(markdown_source.as_str(), Options::all()); let result = GardenParser::parse(&markdown_source);
let mut html_output = String::new();
html::push_html(&mut html_output, parser);
// TODO!
let h1_regex = Regex::new(r"<h1>([^>]+)</h1>").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("???"),
};
pages.insert( pages.insert(
path.clone(), String::from(path.to_str().unwrap()),
ParsedPage { ParsedPage {
timestamp: mtime, timestamp: mtime,
html: html_output.clone(), html: result.html,
title: String::from(title), links: result.links,
links: vec![], // todo!, 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<P: AsRef<Path>>(
Ok(result) Ok(result)
} }
struct GardenParser<'a> {
parser: Parser<'a>,
last_nontext_event: Option<Event<'a>>,
current_top_heading: u32,
top_heading_text: &'a mut Option<String>,
links: &'a mut Vec<String>,
}
struct ParseResult {
html: String,
title: Option<String>,
links: Vec<String>,
}
impl<'a> GardenParser<'a> {
fn parse<S: AsRef<str>>(text: &'a S) -> ParseResult {
let mut title: Option<String> = None;
let mut links: Vec<String> = 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<Self::Item> {
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 { fn preprocess_markdown(string: String) -> String {
let double_brackets = Regex::new(r"\[\[(?P<inner>[\w .]+)\]\]").unwrap(); let double_brackets = Regex::new(r"\[\[(?P<inner>[\w .]+)\]\]").unwrap();
let finder = LinkFinder::new(); let finder = LinkFinder::new();
@ -351,3 +422,9 @@ fn preprocess_markdown(string: String) -> String {
result_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))
}

View file

@ -28,6 +28,17 @@
<main> <main>
{{content | safe}} {{content | safe}}
<hr>
{% if backlinks %}
<h1>Linked from:</h1>
<ul>
{% for link in backlinks %}
<li><a href="/{{ link }}">{{link}}</a></li>
{% endfor %}
</ul>
{% endif %}
{% if mtime %} {% if mtime %}
<footer> <footer>
Last modified at {{mtime}} Last modified at {{mtime}}