use globset::{Glob, GlobSet, GlobSetBuilder};
use log::{debug, info, trace};
use pathdiff::diff_paths;
use serde_json::json;
use std::cell::RefMut;
use std::default::Default;
use std::fs;
use std::io::{BufRead, BufReader, Read, Write};
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
pub use log;
pub use serde_json as json;
pub use std::collections::HashMap;
pub use ware::Ware;
pub type RefIR<'a> = RefMut<'a, IR>;
pub type Plugin = Box<dyn Fn(RefIR) -> ()>;
mod frontmatter;
pub mod plugins;
pub struct Shtola {
ware: Ware<IR>,
ir: IR,
}
impl Shtola {
pub fn new() -> Shtola {
let config: Config = Default::default();
let ir = IR {
files: HashMap::new(),
config,
metadata: HashMap::new(),
};
Shtola {
ware: Ware::new(),
ir,
}
}
pub fn ignores(&mut self, vec: &mut Vec<String>) {
self.ir.config.ignores.append(vec);
self.ir.config.ignores.dedup();
}
pub fn source_ignores(&mut self, path: &Path) -> Result<(), std::io::Error> {
let sourcepath = self.ir.config.source.clone().canonicalize()?;
let file = fs::File::open(path)?;
let reader = BufReader::new(file);
let mut ignores: Vec<String> = reader.lines().map(|l| l.unwrap()).collect();
self.ignores(&mut ignores);
self.ignores(&mut vec![path
.canonicalize()?
.strip_prefix(sourcepath)
.unwrap_or(path)
.to_str()
.unwrap()
.to_string()]);
Ok(())
}
pub fn source<T: Into<PathBuf>>(&mut self, path: T) {
self.ir.config.source = fs::canonicalize(path.into()).unwrap();
}
pub fn destination<T: Into<PathBuf> + Clone>(&mut self, path: T) {
fs::create_dir_all(path.clone().into()).expect("Unable to create destination directory!");
self.ir.config.destination = fs::canonicalize(path.into()).unwrap();
}
pub fn clean(&mut self, b: bool) {
self.ir.config.clean = b;
}
pub fn frontmatter(&mut self, b: bool) {
self.ir.config.frontmatter = b;
}
pub fn register(&mut self, func: Box<dyn Fn(RefIR)>) {
self.ware.wrap(func);
}
pub fn build(&mut self) -> Result<IR, std::io::Error> {
trace!("Starting IR config: {:?}", self.ir.config);
if self.ir.config.clean {
info!("Cleaning before build...");
debug!("Removing {:?}", &self.ir.config.destination);
fs::remove_dir_all(&self.ir.config.destination)?;
debug!("Recreating {:?}", &self.ir.config.destination);
fs::create_dir_all(&self.ir.config.destination)
.expect("Unable to recreate destination directory!");
}
let path_clone = self.ir.config.destination.clone();
let pathstr = path_clone.to_str().expect("No destination provided!");
self.ignores(&mut vec![pathstr.to_string()]);
let mut builder = GlobSetBuilder::new();
for item in &self.ir.config.ignores {
builder.add(Glob::new(item).unwrap());
}
let set = builder.build().unwrap();
info!("Reading files...");
let files = read_dir(&self.ir.config.source, self.ir.config.frontmatter, set)?;
trace!("{} file(s) read", &files.len());
self.ir.files = files;
info!("Running plugins...");
let result_ir = self.ware.run(self.ir.clone());
info!("Writing to disk...");
write_dir(result_ir.clone(), &self.ir.config.destination)?;
info!("OK, done");
Ok(result_ir)
}
}
#[derive(Debug, Clone)]
pub struct IR {
pub files: HashMap<PathBuf, ShFile>,
pub config: Config,
pub metadata: HashMap<String, json::Value>,
}
#[derive(Debug, Clone)]
pub struct Config {
pub ignores: Vec<String>,
pub source: PathBuf,
pub destination: PathBuf,
pub clean: bool,
pub frontmatter: bool,
}
impl Default for Config {
fn default() -> Self {
Config {
ignores: vec![".git/**/*".into()],
source: PathBuf::from("."),
destination: PathBuf::from("./dest"),
clean: false,
frontmatter: true,
}
}
}
#[derive(Debug, Clone)]
pub struct ShFile {
pub frontmatter: json::Value,
pub content: Vec<u8>,
pub raw_content: Option<Vec<u8>>,
}
impl Default for ShFile {
fn default() -> ShFile {
ShFile {
content: Vec::new(),
frontmatter: json::Value::Null,
raw_content: None,
}
}
}
impl ShFile {
pub fn empty() -> ShFile {
ShFile {
frontmatter: json!(null),
content: Vec::new(),
raw_content: None,
}
}
}
fn read_dir(
source: &PathBuf,
frontmatter: bool,
set: GlobSet,
) -> Result<HashMap<PathBuf, ShFile>, std::io::Error> {
let mut result = HashMap::new();
let iters = WalkDir::new(source)
.into_iter()
.filter_entry(|e| {
let path = diff_paths(e.path(), source).unwrap();
!set.is_match(path)
})
.filter(|e| !e.as_ref().ok().unwrap().file_type().is_dir());
for entry in iters {
let entry = entry?;
let path = entry.path();
let file: ShFile;
let mut content = Vec::new();
debug!("Reading file at {:?}", &path);
fs::File::open(path)?.read_to_end(&mut content)?;
if let Ok(content_string) = std::str::from_utf8(&content) {
if frontmatter {
let (matter, content) = frontmatter::lexer(content_string);
if matter.len() > 0 {
debug!("Lexing frontmatter for {:?}", &path);
}
let json = frontmatter::to_json(&matter);
file = ShFile {
frontmatter: json,
content: content.into(),
raw_content: None,
};
} else {
file = ShFile {
frontmatter: json!(null),
content,
raw_content: None,
};
}
} else {
file = ShFile {
frontmatter: json!(null),
content: Vec::new(),
raw_content: Some(content),
}
}
let rel_path = diff_paths(path, source).unwrap();
result.insert(rel_path, file);
}
Ok(result)
}
fn write_dir(ir: IR, dest: &PathBuf) -> Result<(), std::io::Error> {
for (path, file) in ir.files {
let dest_path = dest.join(&path);
debug!("Writing {:?} to {:?}", &path, &dest_path);
fs::create_dir_all(dest_path.parent().unwrap())
.expect("Unable to create destination subdirectory!");
if let Some(raw) = file.raw_content {
fs::File::create(dest_path)?.write_all(&raw)?;
} else {
fs::File::create(dest_path)?.write_all(&file.content)?;
}
}
Ok(())
}