use crate::{config::Config, parser, rss, models::Post};
use rayon::prelude::*;
use serde_json::json;
use std::{
collections::HashMap,
fs, io,
path::{Path},
sync::{Arc, Mutex},
time::{Instant, SystemTime},
};
use syntect::highlighting::ThemeSet;
use syntect::parsing::SyntaxSet;
fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()> {
fs::create_dir_all(&dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let ty = entry.file_type()?;
if ty.is_dir() {
copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
} else {
fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
}
}
Ok(())
}
pub fn compile_sass(config: &Config, verbose: bool) -> io::Result<()> {
let sass_dir = Path::new("sass");
if !sass_dir.exists() {
return Ok(());
}
let css_dir = config.output_dir.join("css");
fs::create_dir_all(&css_dir)?;
let style = match config.build.sass_style.as_str() {
"compressed" => grass::OutputStyle::Compressed,
_ => grass::OutputStyle::Expanded,
};
let options = grass::Options::default().style(style);
for entry in fs::read_dir(sass_dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().map_or(false, |ext| ext == "scss" || ext == "sass") {
let file_name = path.file_name().unwrap().to_str().unwrap();
if file_name.starts_with('_') {
continue;
}
if verbose {
println!("\x1b[2m compiling\x1b[0m {}", file_name);
}
match grass::from_path(&path, &options) {
Ok(css) => {
let mut out_path = css_dir.join(file_name);
out_path.set_extension("css");
fs::write(out_path, css)?;
}
Err(e) => {
return Err(io::Error::new(io::ErrorKind::Other, format!("Sass Error: {}", e)));
}
}
}
}
Ok(())
}
pub fn perform_build(
config: &Config,
last_run_mu: Arc<Mutex<SystemTime>>,
verbose: bool,
) -> io::Result<()> {
let start = Instant::now();
let lr = *last_run_mu.lock().unwrap();
println!("novos build v{}", env!("CARGO_PKG_VERSION"));
let ps = SyntaxSet::load_defaults_newlines();
let ts = ThemeSet::load_defaults();
let theme = ts.themes.get(&config.build.syntax_theme)
.or_else(|| ts.themes.get("base16-ocean.dark"))
.expect("Failed to load syntax theme");
if config.build.clean_output {
println!("\x1b[2m[1/4]\x1b[0m Cleaning output directory...");
if config.output_dir.exists() {
let _ = fs::remove_dir_all(&config.output_dir);
}
} else {
println!("\x1b[2m[1/4]\x1b[0m Preparing output directory...");
}
fs::create_dir_all(&config.output_dir)?;
let posts_out_path = config.output_dir.join(&config.posts_outdir);
fs::create_dir_all(&posts_out_path)?;
if config.static_dir.exists() {
if verbose {
println!("\x1b[2m copying assets\x1b[0m");
}
copy_dir_all(&config.static_dir, &config.output_dir)?;
}
println!("\x1b[2m[2/4]\x1b[0m Compiling stylesheets...");
compile_sass(config, verbose)?;
println!("\x1b[2m[3/4]\x1b[0m Processing content...");
let main_tpl = fs::read_to_string(&config.template_path)?;
let view_tpl = fs::read_to_string(&config.view_template_path).unwrap_or_else(|_| "<% content %>".to_string());
let mut post_paths = Vec::new();
if config.posts_dir.exists() {
for e in fs::read_dir(&config.posts_dir)? {
let p = e?.path();
if p.extension().map(|s| s == "md").unwrap_or(false) {
post_paths.push(p);
}
}
}
let mut posts: Vec<Post> = post_paths
.into_par_iter()
.map(|p| {
let mt = fs::metadata(&p).and_then(|m| m.modified()).unwrap_or(lr);
let raw = fs::read_to_string(&p).unwrap_or_default();
parser::parse_frontmatter(&raw, p.file_stem().unwrap().to_str().unwrap(), mt)
})
.collect();
posts.sort_by(|a, b| b.date.cmp(&a.date));
let mut posts_html = String::from("<ul class='post-list'>\n");
for p in &posts {
let link_path = if config.posts_outdir.is_empty() {
format!("{}{}.html", config.base, p.slug)
} else {
format!("{}{}/{}.html", config.base, config.posts_outdir, p.slug)
};
posts_html.push_str(&format!(
" <li>{} - <a href='{}'>{}</a></li>\n",
p.date, link_path, p.title
));
}
posts_html.push_str("</ul>");
println!("\x1b[2m[4/4]\x1b[0m Rendering pages...");
posts.par_iter().for_each(|p| {
let dest = posts_out_path.join(format!("{}.html", p.slug));
if p.mtime > lr || !dest.exists() {
let mut vars = HashMap::new();
let body = parser::render_markdown(&p.raw_content, config.build.use_syntect, &ps, theme);
let layout = parser::resolve_tags(&view_tpl, config, &posts_html, p, Some(&body), 0, &mut vars);
let final_h = parser::resolve_tags(&main_tpl, config, &posts_html, p, Some(&layout), 0, &mut vars);
fs::write(dest, final_h).ok();
}
});
if config.site.generate_rss {
if verbose { println!("\x1b[2m generating rss\x1b[0m"); }
let rss_xml = rss::generate_rss(&posts, config);
fs::write(config.output_dir.join("rss.xml"), rss_xml)?;
}
if config.site.generate_search {
if verbose { println!("\x1b[2m indexing search content\x1b[0m"); }
let search_index: Vec<serde_json::Value> = posts.iter().map(|p| {
let clean_text = parser::strip_markdown(&p.raw_content);
let snippet: String = clean_text.chars().take(140).collect();
json!({
"title": p.title,
"slug": p.slug,
"date": p.date,
"tags": p.tags,
"snippet": snippet
})
}).collect();
fs::write(config.output_dir.join("search.json"), serde_json::to_string(&search_index)?)?;
}
if config.pages_dir.exists() {
let mut page_paths = Vec::new();
for e in fs::read_dir(&config.pages_dir)? {
let p = e?.path();
if p.extension().map(|s| s == "html").unwrap_or(false) {
page_paths.push(p);
}
}
page_paths.into_par_iter().for_each(|p| {
let slug = p.file_stem().unwrap().to_str().unwrap();
let raw = fs::read_to_string(&p).unwrap_or_default();
let mt = fs::metadata(&p).and_then(|m| m.modified()).unwrap_or(lr);
let pd = parser::parse_frontmatter(&raw, slug, mt);
let mut vars = HashMap::new();
let pb = parser::resolve_tags(&pd.raw_content, config, &posts_html, &pd, None, 0, &mut vars);
let fh = parser::resolve_tags(&main_tpl, config, &posts_html, &pd, Some(&pb), 0, &mut vars);
fs::write(config.output_dir.join(format!("{}.html", slug)), fh).ok();
});
}
let index_meta = Post {
slug: "index".to_string(),
title: config.site.title.clone(),
date: "".to_string(),
tags: vec![],
raw_content: "<% include home.html %>".to_string(),
mtime: SystemTime::now(),
};
let mut index_vars = HashMap::new();
let index_body = parser::resolve_tags(&index_meta.raw_content, config, &posts_html, &index_meta, None, 0, &mut index_vars);
let index_page = parser::resolve_tags(&main_tpl, config, &posts_html, &index_meta, Some(&index_body), 0, &mut index_vars);
fs::write(config.output_dir.join("index.html"), index_page)?;
if let Ok(mut lr_lock) = last_run_mu.lock() {
*lr_lock = SystemTime::now();
}
println!("\x1b[36msuccess\x1b[0m build complete.");
println!("Done in {:.2}s.", start.elapsed().as_secs_f32());
Ok(())
}