add backlinks
This commit is contained in:
parent
9d8f308a4d
commit
7d097ebe72
2 changed files with 132 additions and 44 deletions
157
src/main.rs
157
src/main.rs
|
@ -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()
|
return Ok(HttpResponse::Found()
|
||||||
.header(http::header::LOCATION, md_path)
|
.header(http::header::LOCATION, format!("{}.md", path.to_string()))
|
||||||
.finish());
|
.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!
|
pages.insert(
|
||||||
let h1_regex = Regex::new(r"<h1>([^>]+)</h1>").unwrap();
|
String::from(path.to_str().unwrap()),
|
||||||
let title = match h1_regex.captures(&html_output) {
|
ParsedPage {
|
||||||
Some(h1_match) => h1_match.get(1).unwrap().as_str(),
|
timestamp: mtime,
|
||||||
_ => &path
|
html: result.html,
|
||||||
.components()
|
links: result.links,
|
||||||
|
title: match result.title {
|
||||||
|
Some(title) => title,
|
||||||
|
_ => String::from(
|
||||||
|
path.components()
|
||||||
.last()
|
.last()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.as_os_str()
|
.as_os_str()
|
||||||
.to_str()
|
.to_str()
|
||||||
.unwrap_or("???"),
|
.unwrap_or("???"),
|
||||||
};
|
),
|
||||||
|
},
|
||||||
pages.insert(
|
|
||||||
path.clone(),
|
|
||||||
ParsedPage {
|
|
||||||
timestamp: mtime,
|
|
||||||
html: html_output.clone(),
|
|
||||||
title: String::from(title),
|
|
||||||
links: vec![], // todo!,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
|
}
|
||||||
|
|
|
@ -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}}
|
||||||
|
|
Loading…
Reference in a new issue