2019-06-30 20:47:04 +00:00
|
|
|
use std::collections::{HashMap, HashSet};
|
|
|
|
use std::path::{Path, PathBuf};
|
2019-06-30 18:59:58 +00:00
|
|
|
use std::sync::mpsc::channel;
|
2019-06-30 22:10:33 +00:00
|
|
|
use std::time::{Duration, Instant};
|
2019-06-30 18:59:58 +00:00
|
|
|
use std::{fs, io};
|
|
|
|
|
2019-06-30 21:46:37 +00:00
|
|
|
use comrak::{self, ComrakOptions};
|
2019-06-30 22:10:33 +00:00
|
|
|
use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
|
2019-06-30 20:47:04 +00:00
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use toml;
|
2019-06-30 18:59:58 +00:00
|
|
|
|
2019-06-30 20:47:04 +00:00
|
|
|
type TagName = String;
|
|
|
|
type TagId = String;
|
|
|
|
type TagMap = HashMap<TagName, TagId>;
|
|
|
|
|
|
|
|
// Maps field to value.
|
|
|
|
type NoteMap = HashMap<String, String>;
|
|
|
|
|
|
|
|
type NotesMap = HashMap<PathBuf, NoteMap>;
|
|
|
|
|
|
|
|
const TAG_TYPE_ID: &'static str = "5";
|
|
|
|
|
2019-06-30 21:46:37 +00:00
|
|
|
const TEMPLATE_HEADER: &'static str = r#"
|
|
|
|
<!DOCTYPE html>
|
|
|
|
<html lang="fr">
|
|
|
|
<head>
|
|
|
|
<link rel="stylesheet" type="text/css" href="https://blog.benj.me/theme/style.css">
|
|
|
|
<link rel="stylesheet" type="text/css" href="https://blog.benj.me/theme/fonts/inter.css">
|
|
|
|
|
|
|
|
<meta charset="utf-8" />
|
|
|
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
|
|
<meta name="HandheldFriendly" content="True" />
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
|
|
<meta name="robots" content="" />
|
|
|
|
|
|
|
|
<title>{{TITLE}}</title>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<main>
|
|
|
|
"#;
|
|
|
|
|
|
|
|
const TEMPLATE_FOOTER: &'static str = r#"
|
|
|
|
</main>
|
|
|
|
</body>
|
|
|
|
</html>
|
|
|
|
"#;
|
|
|
|
|
2019-06-30 20:47:04 +00:00
|
|
|
#[derive(Serialize, Deserialize)]
|
|
|
|
struct Configuration {
|
|
|
|
path_in: String,
|
|
|
|
path_out: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
struct Context {
|
|
|
|
config: Configuration,
|
|
|
|
tags: Option<TagMap>,
|
|
|
|
public_tag_id: Option<String>,
|
2019-06-30 18:59:58 +00:00
|
|
|
}
|
|
|
|
|
2019-06-30 20:47:04 +00:00
|
|
|
impl Context {
|
|
|
|
fn new(config: Configuration) -> Self {
|
|
|
|
Self {
|
|
|
|
config,
|
|
|
|
tags: None,
|
|
|
|
public_tag_id: None,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn update_tags(&mut self, notes: &NotesMap) {
|
2019-06-30 21:46:37 +00:00
|
|
|
let tags = collect_tags(notes);
|
2019-06-30 20:47:04 +00:00
|
|
|
self.public_tag_id = tags.get("public").map(|x| x.clone());
|
|
|
|
self.tags = Some(tags);
|
|
|
|
}
|
|
|
|
|
2019-06-30 23:06:19 +00:00
|
|
|
fn sanitize_title(title: &str) -> String {
|
|
|
|
// TODO do a better sanitize of the title.
|
|
|
|
title.replace(" ", "_")
|
|
|
|
}
|
|
|
|
|
2019-06-30 20:47:04 +00:00
|
|
|
fn publish_public(&self, notes: &NotesMap) -> Result<(), io::Error> {
|
|
|
|
let public_tag_id = if let Some(id) = &self.public_tag_id {
|
|
|
|
id
|
|
|
|
} else {
|
|
|
|
return Ok(());
|
|
|
|
};
|
|
|
|
|
|
|
|
let mut tagged_notes = HashSet::new();
|
|
|
|
for note in notes.values() {
|
|
|
|
if let Some(tag_id) = note.get("tag_id") {
|
|
|
|
if tag_id == public_tag_id {
|
|
|
|
tagged_notes.insert(note.get("note_id").unwrap());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let tagged_notes = tagged_notes.iter().map(|¬e_id| {
|
|
|
|
notes
|
|
|
|
.iter()
|
|
|
|
.find(|(_, note)| ¬e["id"] == note_id)
|
|
|
|
.map(|(path, _)| path)
|
|
|
|
.unwrap()
|
|
|
|
});
|
|
|
|
|
|
|
|
for note_id in tagged_notes {
|
|
|
|
let note = ¬es[note_id];
|
|
|
|
if let Some(content) = note.get("markdown_content") {
|
2019-06-30 21:46:37 +00:00
|
|
|
// Put the title as H1, if it's not the case yet.
|
|
|
|
let md_content = if !content.starts_with("#") {
|
|
|
|
"# ".to_string() + content
|
|
|
|
} else {
|
|
|
|
content.to_string()
|
|
|
|
};
|
|
|
|
|
|
|
|
let title = content.split("\n").next().unwrap();
|
2019-06-30 20:47:04 +00:00
|
|
|
|
2019-06-30 22:10:33 +00:00
|
|
|
let mut options = ComrakOptions::default();
|
|
|
|
options.ext_autolink = true;
|
|
|
|
options.ext_tasklist = true;
|
|
|
|
options.ext_header_ids = Some("id-".into());
|
|
|
|
|
2019-06-30 21:46:37 +00:00
|
|
|
let html_content = TEMPLATE_HEADER.replace("{{TITLE}}", &title)
|
2019-06-30 22:10:33 +00:00
|
|
|
+ &comrak::markdown_to_html(&md_content, &options)
|
2019-06-30 21:46:37 +00:00
|
|
|
+ TEMPLATE_FOOTER;
|
2019-06-30 20:47:04 +00:00
|
|
|
|
2019-06-30 23:06:19 +00:00
|
|
|
let filename = Context::sanitize_title(title);
|
2019-06-30 21:46:37 +00:00
|
|
|
let path = self.config.path_out.clone() + &format!("/{}.html", filename);
|
|
|
|
|
|
|
|
println!("Publishing {}", path);
|
|
|
|
|
|
|
|
let mut file = fs::File::create(path)?;
|
2019-06-30 22:10:33 +00:00
|
|
|
|
|
|
|
use std::io::Write;
|
2019-06-30 21:46:37 +00:00
|
|
|
file.write_all(html_content.as_bytes())?;
|
2019-06-30 20:47:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2019-06-30 23:06:19 +00:00
|
|
|
|
|
|
|
fn remove_public(&mut self, note: &NoteMap) {
|
|
|
|
// TODO this will only remove the file if the note has been removed, not if the public tag
|
|
|
|
// has been removed. Handle this too.
|
|
|
|
let title = if let Some(content) = note.get("markdown_content") {
|
|
|
|
content.split("\n").next().unwrap()
|
|
|
|
} else {
|
|
|
|
return;
|
|
|
|
};
|
|
|
|
|
|
|
|
let filename = Context::sanitize_title(&title);
|
|
|
|
let path = self.config.path_out.clone() + &format!("/{}.html", filename);
|
|
|
|
|
|
|
|
// Missing files or errors on deletion are fine.
|
|
|
|
let _ = fs::remove_file(&path);
|
|
|
|
}
|
2019-06-30 20:47:04 +00:00
|
|
|
}
|
|
|
|
|
2019-06-30 21:46:37 +00:00
|
|
|
fn collect_tags(notes: &NotesMap) -> TagMap {
|
2019-06-30 20:47:04 +00:00
|
|
|
let mut tags = TagMap::new();
|
|
|
|
|
|
|
|
for note in notes.values() {
|
|
|
|
if let Some(type_id) = note.get("type_") {
|
|
|
|
if type_id == TAG_TYPE_ID {
|
|
|
|
tags.insert(note["markdown_content"].clone(), note["id"].clone());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
tags
|
|
|
|
}
|
|
|
|
|
|
|
|
fn run_hooks(context: &mut Context, notes: &NotesMap) -> Result<(), io::Error> {
|
2019-06-30 22:10:33 +00:00
|
|
|
println!("Running hooks...");
|
|
|
|
let start = Instant::now();
|
|
|
|
|
2019-06-30 20:47:04 +00:00
|
|
|
context.update_tags(notes);
|
2019-06-30 22:10:33 +00:00
|
|
|
let ret = context.publish_public(notes);
|
|
|
|
|
|
|
|
let duration = Instant::now() - start;
|
|
|
|
println!("Took {}ms", duration.as_millis());
|
|
|
|
|
|
|
|
ret
|
2019-06-30 20:47:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn read_file(path: &Path) -> Result<NoteMap, io::Error> {
|
2019-06-30 18:59:58 +00:00
|
|
|
let content = fs::read_to_string(path)?;
|
|
|
|
let content = content.trim().split("\n").collect::<Vec<_>>();
|
|
|
|
|
2019-06-30 20:47:04 +00:00
|
|
|
let mut fields = NoteMap::new();
|
2019-06-30 18:59:58 +00:00
|
|
|
|
|
|
|
for j in 0..content.len() {
|
|
|
|
let i = content.len() - 1 - j;
|
|
|
|
let line = content[i];
|
|
|
|
|
|
|
|
if let Some(index) = line.find(":") {
|
|
|
|
let (key, value) = line.split_at(index);
|
|
|
|
// Remove the ":" character.
|
|
|
|
let value = value[1..].trim();
|
|
|
|
if key.len() > 0 && value.len() > 0 {
|
|
|
|
fields.insert(key.to_string(), value.to_string());
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// First line without a metadata field! Reverse content, it's the markdown block.
|
|
|
|
let markdown = content[0..i]
|
|
|
|
.iter()
|
|
|
|
.map(|s| s.to_string())
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
.join("\n");
|
|
|
|
let markdown = markdown.trim();
|
|
|
|
fields.insert("markdown_content".to_string(), markdown.to_string());
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
println!(
|
|
|
|
"Extracted content of {} ({} fields)",
|
2019-06-30 21:46:37 +00:00
|
|
|
fields
|
|
|
|
.get("id")
|
|
|
|
.expect(&format!("Missing id for note {}", path.to_string_lossy())),
|
2019-06-30 18:59:58 +00:00
|
|
|
fields.len()
|
|
|
|
);
|
|
|
|
|
|
|
|
Ok(fields)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn main() -> Result<(), io::Error> {
|
2019-06-30 20:47:04 +00:00
|
|
|
let config_content = fs::read_to_string("./config.toml")?;
|
|
|
|
let config: Configuration = toml::from_str(&config_content).unwrap();
|
|
|
|
|
|
|
|
match fs::metadata(&config.path_out) {
|
|
|
|
Ok(_) => {}
|
|
|
|
Err(_) => {
|
|
|
|
fs::create_dir(&config.path_out)?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-30 18:59:58 +00:00
|
|
|
let mut num_files = 0;
|
2019-06-30 20:47:04 +00:00
|
|
|
let mut files = NotesMap::new();
|
2019-06-30 18:59:58 +00:00
|
|
|
|
2019-06-30 20:47:04 +00:00
|
|
|
for entry in fs::read_dir(&config.path_in)? {
|
2019-06-30 18:59:58 +00:00
|
|
|
let dir_entry = entry?;
|
|
|
|
if !dir_entry.file_type()?.is_file() {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
let path = dir_entry.path();
|
2019-06-30 21:46:37 +00:00
|
|
|
let note = read_file(&path)?;
|
|
|
|
files.insert(path, note);
|
2019-06-30 18:59:58 +00:00
|
|
|
num_files += 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
println!("Found {} files.", num_files);
|
|
|
|
|
2019-06-30 20:47:04 +00:00
|
|
|
let mut context = Context::new(config);
|
|
|
|
run_hooks(&mut context, &files)?;
|
|
|
|
|
2019-06-30 18:59:58 +00:00
|
|
|
println!("Now listening to disk events...");
|
|
|
|
|
|
|
|
let (tx, rx) = channel();
|
|
|
|
let mut watcher: RecommendedWatcher = Watcher::new(tx, Duration::from_secs(2)).unwrap();
|
|
|
|
|
|
|
|
watcher
|
2019-06-30 21:52:25 +00:00
|
|
|
.watch(&context.config.path_in, RecursiveMode::Recursive)
|
2019-06-30 18:59:58 +00:00
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
loop {
|
|
|
|
match rx.recv() {
|
|
|
|
Ok(evt) => {
|
2019-06-30 22:10:33 +00:00
|
|
|
use DebouncedEvent::*;
|
2019-06-30 23:06:19 +00:00
|
|
|
match &evt {
|
2019-06-30 22:10:33 +00:00
|
|
|
Create(path) | Write(path) => {
|
2019-06-30 18:59:58 +00:00
|
|
|
files.insert(path.clone(), read_file(path)?);
|
2019-06-30 23:06:19 +00:00
|
|
|
run_hooks(&mut context, &files)?;
|
2019-06-30 18:59:58 +00:00
|
|
|
}
|
2019-06-30 21:46:37 +00:00
|
|
|
Remove(path) => {
|
2019-06-30 23:06:19 +00:00
|
|
|
if let Some(note) = files.get(path) {
|
|
|
|
context.remove_public(note);
|
|
|
|
}
|
2019-06-30 18:59:58 +00:00
|
|
|
files.remove(path);
|
|
|
|
}
|
2019-06-30 23:06:19 +00:00
|
|
|
_ => {}
|
2019-06-30 22:10:33 +00:00
|
|
|
};
|
2019-06-30 18:59:58 +00:00
|
|
|
println!("{:?}", evt);
|
|
|
|
}
|
|
|
|
Err(e) => println!("watch error: {:?}", e),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|