You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
211 lines
5.8 KiB
211 lines
5.8 KiB
use std::collections::{HashMap, HashSet}; |
|
use std::path::{Path, PathBuf}; |
|
use std::sync::mpsc::channel; |
|
use std::time::Duration; |
|
use std::{fs, io}; |
|
|
|
use markdown; |
|
use notify::{ |
|
DebouncedEvent::{Create, NoticeRemove, NoticeWrite, Remove}, |
|
RecommendedWatcher, RecursiveMode, Watcher, |
|
}; |
|
use serde::{Deserialize, Serialize}; |
|
use toml; |
|
|
|
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"; |
|
|
|
#[derive(Serialize, Deserialize)] |
|
struct Configuration { |
|
path_in: String, |
|
path_out: String, |
|
} |
|
|
|
struct Context { |
|
config: Configuration, |
|
tags: Option<TagMap>, |
|
public_tag_id: Option<String>, |
|
} |
|
|
|
impl Context { |
|
fn new(config: Configuration) -> Self { |
|
Self { |
|
config, |
|
tags: None, |
|
public_tag_id: None, |
|
} |
|
} |
|
|
|
fn update_tags(&mut self, notes: &NotesMap) { |
|
let tags = analyze_tags(notes); |
|
self.public_tag_id = tags.get("public").map(|x| x.clone()); |
|
self.tags = Some(tags); |
|
} |
|
|
|
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") { |
|
println!("Gonna publish this content: {}", content); |
|
|
|
// TODO crash in markdown! |
|
let content = markdown::to_html(content); |
|
|
|
// TODO do a better sanitize of the title. |
|
let title = content.split("\n").next().unwrap().replace(" ", "_"); |
|
let path = self.config.path_out.clone() + &format!("/{}.html", title); |
|
fs::write(path, content)?; |
|
} |
|
} |
|
|
|
Ok(()) |
|
} |
|
} |
|
|
|
fn analyze_tags(notes: &NotesMap) -> TagMap { |
|
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> { |
|
context.update_tags(notes); |
|
context.publish_public(notes) |
|
} |
|
|
|
fn read_file(path: &Path) -> Result<NoteMap, io::Error> { |
|
let content = fs::read_to_string(path)?; |
|
let content = content.trim().split("\n").collect::<Vec<_>>(); |
|
|
|
let mut fields = NoteMap::new(); |
|
|
|
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)", |
|
fields["id"], |
|
fields.len() |
|
); |
|
|
|
Ok(fields) |
|
} |
|
|
|
fn main() -> Result<(), io::Error> { |
|
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)?; |
|
} |
|
} |
|
|
|
let mut num_files = 0; |
|
let mut files = NotesMap::new(); |
|
|
|
for entry in fs::read_dir(&config.path_in)? { |
|
let dir_entry = entry?; |
|
if !dir_entry.file_type()?.is_file() { |
|
continue; |
|
} |
|
let path = dir_entry.path(); |
|
let read_file = read_file(&path)?; |
|
files.insert(path, read_file); |
|
num_files += 1; |
|
} |
|
|
|
println!("Found {} files.", num_files); |
|
|
|
let mut context = Context::new(config); |
|
run_hooks(&mut context, &files)?; |
|
|
|
println!("Now listening to disk events..."); |
|
|
|
let (tx, rx) = channel(); |
|
let mut watcher: RecommendedWatcher = Watcher::new(tx, Duration::from_secs(2)).unwrap(); |
|
|
|
watcher |
|
.watch("./assets/", RecursiveMode::Recursive) |
|
.unwrap(); |
|
|
|
loop { |
|
match rx.recv() { |
|
Ok(evt) => { |
|
match &evt { |
|
NoticeWrite(path) | Create(path) => { |
|
files.insert(path.clone(), read_file(path)?); |
|
} |
|
NoticeRemove(path) | Remove(path) => { |
|
files.remove(path); |
|
} |
|
_ => {} |
|
} |
|
run_hooks(&mut context, &files)?; |
|
println!("{:?}", evt); |
|
} |
|
Err(e) => println!("watch error: {:?}", e), |
|
} |
|
} |
|
}
|
|
|