237 lines
8.6 KiB
Rust
237 lines
8.6 KiB
Rust
|
|
use std::collections::HashMap;
|
|
use chrono::NaiveDateTime;
|
|
use html5ever::tendril::SliceExt;
|
|
use nipper::{Document, Selection};
|
|
use rayon::prelude::*;
|
|
use regex::Regex;
|
|
use serde::{Serialize, Deserialize};
|
|
|
|
use std::env::current_dir;
|
|
use std::fs;
|
|
use std::fs::{read_to_string, File};
|
|
use std::io::ErrorKind::NotFound;
|
|
use std::io::{Error, Write};
|
|
use std::path::PathBuf;
|
|
use meowhash::MeowHasher;
|
|
|
|
extern crate markdown;
|
|
|
|
fn main() -> Result<(), Error> {
|
|
let schema_version = 1;
|
|
println!("Looking for a typeset.toml file...");
|
|
let cwd = current_dir()?;
|
|
let mut config_update = false;
|
|
let config_file = cwd.join("typeset.toml");
|
|
if !config_file.exists() {
|
|
eprintln!("typeset.toml not found!");
|
|
return Err(Error::from(NotFound));
|
|
}
|
|
let config_body = read_to_string(config_file.clone())?;
|
|
let config_hash = MeowHasher::hash(config_body.as_bytes()).into_bytes();
|
|
let config: Config = toml::from_str(&*config_body)?;
|
|
|
|
if schema_version != config.schema_version {
|
|
panic!("Schema version does not match this version of Typeset! Please update Typeset.")
|
|
}
|
|
let template_file = config_file.with_file_name(config.template);
|
|
let index_file = config_file.with_file_name(config.index);
|
|
let hash_file = config_file.with_file_name("hashes.toml");
|
|
|
|
let out_path = cwd.join(PathBuf::from(config.output));
|
|
if !hash_file.exists() {
|
|
println!("Creating a new blog, since no hashes have been calculated");
|
|
config_update = true;
|
|
}
|
|
let hash_body = read_to_string(hash_file.clone()).unwrap_or(String::new());
|
|
let hash_body: HashFile = toml::from_str(&*hash_body).unwrap_or(HashFile{hashes: HashMap::new()});
|
|
let in_hashes: HashMap<String, [u8; 128]> = hash_body.hashes.par_iter().map(|(k, v)| {
|
|
let mut array = [0u8; 128];
|
|
let decoded = base64::decode(v).unwrap();
|
|
let slice = decoded.as_slice();
|
|
array = <[u8; 128]>::try_from(slice).unwrap();
|
|
(k.clone(), array)
|
|
}).collect::<HashMap<String, [u8; 128]>>();
|
|
let mut out_hashes: HashMap<String, [u8; 128]> = HashMap::new();
|
|
if !template_file.exists() {
|
|
eprintln!("template does not exist!");
|
|
return Err(Error::from(NotFound));
|
|
}
|
|
if !index_file.exists() {
|
|
println!("Index not found, assuming it is not present")
|
|
}
|
|
|
|
if in_hashes.get("config").unwrap_or(&[0; 128]) != &config_hash {
|
|
config_update = true;
|
|
out_hashes.insert("config".to_string(), config_hash);
|
|
}
|
|
let template = read_to_string(template_file)?;
|
|
let template_hash = MeowHasher::hash(template.as_bytes()).into_bytes();
|
|
if in_hashes.get("template").unwrap_or(&[0; 128]) != &template_hash {
|
|
config_update = true;
|
|
out_hashes.insert("template".to_string(), template_hash);
|
|
}
|
|
let index_body = read_to_string(index_file)?;
|
|
let index_hash = MeowHasher::hash(index_body.as_bytes()).into_bytes();
|
|
if in_hashes.get("index").unwrap_or(&[0; 128]) != &index_hash {
|
|
config_update = true;
|
|
out_hashes.insert("index".to_string(), index_hash);
|
|
}
|
|
let index = Document::from(&*index_body);
|
|
println!("Read configuration successfully! Converting markdown files to html...");
|
|
|
|
let mut posts: Vec<Post> = vec![];
|
|
let mut posts_to_index: Vec<Post> = vec![];
|
|
let input_files: Vec<PathBuf> = fs::read_dir(cwd)?
|
|
.filter(|file| {
|
|
let file = file.as_ref().unwrap();
|
|
Regex::new(&*config.input)
|
|
.expect("invalid input regex!")
|
|
.is_match(file.file_name().to_str().unwrap())
|
|
})
|
|
.map(|file| file.unwrap().path())
|
|
.collect();
|
|
println!("Found {} files matching the pattern", input_files.len());
|
|
for file in input_files {
|
|
let content = read_to_string(&file).unwrap();
|
|
let hash = MeowHasher::hash(content.as_bytes()).into_bytes();
|
|
let content = content.splitn(2, "\n\n").collect::<Vec<&str>>();
|
|
let settings: PostSettings = toml::from_str(content[0]).unwrap();
|
|
let body = markdown::to_html(content[1]);
|
|
let post = Post {
|
|
id: file.file_stem().unwrap().to_str().unwrap().to_string(),
|
|
body,
|
|
title: settings.title,
|
|
published: NaiveDateTime::parse_from_str(&*settings.published, &*config.time_format)
|
|
.unwrap(),
|
|
};
|
|
posts_to_index.push(post.clone());
|
|
if in_hashes.get(&*post.id).unwrap_or(&[0; 128]) != &hash || config_update {
|
|
out_hashes.insert(post.id.clone(), hash);
|
|
posts.push(post.clone());
|
|
}
|
|
};
|
|
let mut write_hashes = in_hashes.clone();
|
|
write_hashes.par_extend(out_hashes.par_iter().map(|(k, v)| {(k.clone(), v.clone())}));
|
|
println!("Writing hash table...");
|
|
let toml = toml::to_string(&HashFile { hashes: write_hashes.iter().map(|(k, v)| {
|
|
(k.clone(), base64::encode(v))
|
|
}).collect::<HashMap<String, String>>()}).unwrap();
|
|
let mut file = File::create(hash_file)?;
|
|
file.write_all(toml.as_bytes())?;
|
|
|
|
println!("Writing index...");
|
|
posts_to_index.par_sort_by(|a, b| b.published.cmp(&a.published));
|
|
println!(
|
|
"Found {} post lists",
|
|
index.select(r#"meta[typeset="index-entry""#).length()
|
|
);
|
|
for index_ref in index.select(r#"meta[typeset="index-entry""#).iter() {
|
|
let n = index_ref
|
|
.attr("content")
|
|
.unwrap_or("".to_tendril())
|
|
.parse()
|
|
.unwrap_or(0);
|
|
let parent = nth_parent(index_ref.clone(), n);
|
|
let parent_html = parent.html();
|
|
for post in posts_to_index.iter() {
|
|
let html = parent_html.to_string();
|
|
|
|
parent.parent().append_html(html);
|
|
nth_children(parent.parent(), n)
|
|
.select(r#"meta[typeset="index-entry""#)
|
|
.replace_with_html(
|
|
format!("<a href=\"./{}.html\">{}</a>\n", post.id, post.title).as_str(),
|
|
);
|
|
}
|
|
parent.parent().children().iter().nth(0).unwrap().remove();
|
|
}
|
|
|
|
let output = index.html().to_string();
|
|
let path = out_path.join(PathBuf::from("index.html"));
|
|
let mut file = File::create(path)?;
|
|
file.write_all(output.as_bytes())?;
|
|
|
|
if out_hashes.len() == 0 {
|
|
println!("Have a nice day :)");
|
|
return Ok(());
|
|
}
|
|
println!("Successfully converted to HTML! Creating {} document(s)...", posts.len());
|
|
for post in &posts {
|
|
let template_html = Document::from(template.as_str());
|
|
let output_html = template_html;
|
|
let typeset_elements = output_html.select("meta[typeset]");
|
|
for mut element in typeset_elements.iter() {
|
|
match element.attr("typeset").unwrap().to_string().as_str() {
|
|
"page-title" => {
|
|
let title = element
|
|
.attr("content")
|
|
.unwrap()
|
|
.replace("$", post.title.as_str());
|
|
|
|
element.replace_with_html(format!("<title>{}</title>", title));
|
|
}
|
|
"title" => {
|
|
element.replace_with_html(post.title.to_string());
|
|
}
|
|
"body" => {
|
|
element.replace_with_html(post.body.to_string());
|
|
}
|
|
"date" => element
|
|
.replace_with_html(post.published.format(&*config.time_format).to_string()),
|
|
_ => {
|
|
eprintln!("Unknown meta typeset element parsed!")
|
|
}
|
|
}
|
|
}
|
|
let output = output_html.html().to_string();
|
|
let path = out_path.join(PathBuf::from(format!("{}.html", &post.id)));
|
|
let mut file = File::create(path)?;
|
|
file.write_all(output.as_bytes())?;
|
|
}
|
|
|
|
println!("Have a nice day :)");
|
|
Ok(())
|
|
}
|
|
#[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd)]
|
|
struct Post {
|
|
id: String,
|
|
body: String,
|
|
title: String,
|
|
published: NaiveDateTime,
|
|
}
|
|
#[derive(Deserialize, Debug)]
|
|
struct PostSettings {
|
|
title: String,
|
|
published: String,
|
|
}
|
|
#[derive(Deserialize, Debug, Clone)]
|
|
pub struct Config {
|
|
pub schema_version: i32,
|
|
pub name: String,
|
|
pub index: String,
|
|
pub template: String,
|
|
pub ref_from_index: String,
|
|
pub input: String,
|
|
pub output: String,
|
|
pub time_format: String,
|
|
}
|
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
pub struct HashFile {
|
|
pub hashes: HashMap<String, String>
|
|
}
|
|
fn nth_parent(selection: Selection, n: usize) -> Selection {
|
|
let mut sel: Selection = selection;
|
|
for _ in 0..n {
|
|
sel = sel.parent();
|
|
}
|
|
return sel;
|
|
}
|
|
fn nth_children(selection: Selection, n: usize) -> Selection {
|
|
let mut sel: Selection = selection;
|
|
for _ in 0..n {
|
|
sel = sel.children();
|
|
}
|
|
return sel;
|
|
}
|