shtola 0.4.1

Minimal static site generator
Documentation
use std::collections::HashMap;
use std::path::PathBuf;
use std::str::from_utf8;

use id_tree::InsertBehavior::*;
use id_tree::Node;
use id_tree::NodeId;
use id_tree::Tree;
use serde_json::Value;
use tera::Context;
use tera::Tera;

use crate::log;
use crate::ShFile;
use crate::{Plugin, RefIR};

// WARNING! This plugin uses a very unoptimized algorithm and lots of unwraps.
// Needs refactoring at some point.

pub fn plugin() -> Plugin {
	Box::new(|mut ir: RefIR| {
		let mut tree: Tree<PathBuf> = Tree::new();
		let mut children: Vec<NodeId> = Vec::new();
		// Empty root node
		let root_id = tree.insert(Node::new(PathBuf::default()), AsRoot).unwrap();

		// Find layouts with no parent layouts
		let ancestor_layouts: HashMap<&PathBuf, NodeId> = ir
			.files
			.iter()
			.filter(|(_, v)| {
				if let Value::Object(obj) = v.frontmatter.clone() {
					obj.contains_key("is_layout") && !obj.contains_key("layout")
				} else {
					false
				}
			})
			.map(|(path, _)| {
				// Add them as secondary nodes and re-map them into a hashmap
				let id = tree
					.insert(Node::new(path.clone()), UnderNode(&root_id))
					.unwrap();
				(path, id)
			})
			.collect();

		ancestor_layouts.iter().for_each(|(_, node_id)| {
			treeify_for(&mut tree, node_id, &ir, &mut children);
		});

		log::debug!("Layout tree:\n{}", print_tree(&tree));

		// Iterate over each child node, and render layouts in reverse order
		// of the ancestor subtree.
		children.iter().for_each(|id| {
			let path = tree.get(id).unwrap().data();
			log::debug!("Rendering {:?}", path);
			let ancestors: Vec<&Node<PathBuf>> = tree.ancestors(&id).unwrap().collect();
			let mut reviter = ancestors.iter().rev().peekable();
			let mut acc: Option<String> = None;
			reviter.next(); // Root node, empty

			if reviter.len() > 1 {
				// Poor man's `iter::fold`, we can't peek inside of an already borrowed iterator
				while let Some(&node) = reviter.next() {
					let mut context = Context::new();
					if let Some(next_node) = reviter.peek() {
						let val = ir.files.get(next_node.data()).unwrap();
						context.insert("content", from_utf8(&val.content).unwrap());

						match acc {
							None => {
								let layout = ir.files.get(node.data()).unwrap();
								acc = Some(
									Tera::one_off(
										from_utf8(&layout.content).unwrap(),
										&context,
										false,
									)
									.unwrap(),
								)
							}
							Some(layout) => {
								acc = Some(Tera::one_off(layout.as_ref(), &context, false).unwrap())
							}
						}
					}
				}
			} else {
				// Direct child of layout, so we just get the layout and put it in the accumulator
				let node = reviter.next().unwrap();
				acc = Some(
					from_utf8(&ir.files.get(node.data()).unwrap().content)
						.unwrap()
						.to_string(),
				);
			}

			// Finally, render the child node into the accumulated layout
			let mut context = Context::new();
			let node_contents = ir.files.get(path).unwrap();
			context.insert("content", from_utf8(&node_contents.content).unwrap());
			let rendered = Tera::one_off(acc.unwrap().as_ref(), &context, false).unwrap();

			// Remove the old file
			let mut file = node_contents.clone();
			ir.files.remove(path);

			// Insert the new file
			file.content = rendered.into();
			ir.files.insert(path.to_path_buf(), file);
		});

		// Remove layouts from files
		tree.traverse_level_order(&root_id)
			.unwrap()
			.for_each(|node| {
				let file = ir.files.get(node.data());
				if let Some(file) = file {
					if let Value::Object(obj) = file.frontmatter.clone() {
						if obj.contains_key("is_layout") {
							ir.files.remove(node.data());
						}
					}
				}
			});
	})
}

fn treeify_for(
	tree: &mut Tree<PathBuf>,
	node: &NodeId,
	ir: &RefIR,
	final_children: &mut Vec<NodeId>,
) {
	let layout_name = tree.get(node).unwrap().data().file_stem().unwrap();
	// Find the layout's children (e.g. HTML files that use this layout)
	let children: HashMap<&PathBuf, &ShFile> = ir
		.files
		.iter()
		.filter(|(_, file)| {
			if let Value::Object(obj) = file.frontmatter.clone() {
				obj.contains_key("layout")
					&& obj.get("layout").unwrap().as_str() == layout_name.to_str()
			} else {
				false
			}
		})
		.collect();

	if !children.is_empty() {
		// Insert children into tree
		children.iter().for_each(|(path, file)| {
			let node_id = tree
				.insert(Node::new(path.clone().to_path_buf()), UnderNode(node))
				.unwrap();

			// If not a layout, insert into final children
			let fm = file.frontmatter.clone();
			let val = fm.as_object().unwrap();
			if !val.contains_key("is_layout") {
				final_children.push(node_id.clone());
			} else {
				// If a layout, recurse
				treeify_for(tree, &node_id, ir, final_children);
			}
		})
	}
}

fn print_tree(tree: &Tree<PathBuf>) -> String {
	let mut s: String = String::new();
	tree.write_formatted(&mut s).unwrap();
	s
}

#[test]
fn it_works() {
	use crate::Shtola;
	use std::str::from_utf8;

	let mut s = Shtola::new();
	s.source("../fixtures/tera_layouts");
	s.destination("../fixtures/tera_layouts/dest");
	s.clean(true);
	s.register(plugin());
	let r = s.build().unwrap();
	let file1 = r.files.get(&PathBuf::from("page.html")).unwrap();
	let file2 = r.files.get(&PathBuf::from("3rdpage.html")).unwrap();
	assert_eq!(
		from_utf8(&file1.content).unwrap(),
		"<h1>top layout</h1>\n\nhello"
	);
	assert_eq!(
		from_utf8(&file2.content).unwrap(),
		"<h1>top layout</h1>\n\n<p>hiii</p>"
	);
}