commit 4026577fd5ad676e9fc4c26fd72f642ff1ccf23e
parent 43db77e0d043da345265e63c1fec9499bc7d3135
Author: marisa <mokou@posteo.de>
Date: Tue, 12 Nov 2019 15:46:02 +0100
Document shtola
Diffstat:
M | shtola/src/lib.rs | | | 115 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------- |
1 file changed, 104 insertions(+), 11 deletions(-)
diff --git a/shtola/src/lib.rs b/shtola/src/lib.rs
@@ -1,4 +1,33 @@
-use globset::{Glob, GlobSetBuilder};
+//! With Shtola, you can build your own static site generators easily. All that
+//! Shtola itself does is read files and frontmatter, run them through a bunch
+//! of user-provided plugins, and write the result back to disk.
+//!
+//! As a demonstration of Shtola's basic piping feature, see this example:
+//! ```
+//! use shtola::Shtola;
+//!
+//! let mut m = Shtola::new();
+//! m.source("../fixtures/simple");
+//! m.destination("../fixtures/dest");
+//! m.clean(true);
+//! m.build().unwrap();
+//! ```
+//!
+//! A "plugin" is just a boxed function that takes an `IR` (intermediate
+//! representation) struct and also returns an `IR` struct. The plugin may
+//! modify the IR by using immutable modification:
+//!
+//! ```
+//! use shtola::{Plugin, IR, ShFile};
+//!
+//! fn plugin() -> Plugin {
+//! Box::new(|ir: IR| {
+//! IR { files: ir.files.update("myFile".into(), ShFile::empty()), ..ir }
+//! })
+//! }
+//! ```
+
+use globset::{Glob, GlobSetBuilder, GlobSet};
use pathdiff::diff_paths;
use serde_json::json;
use std::default::Default;
@@ -15,12 +44,14 @@ mod frontmatter;
#[cfg(test)]
mod tests;
+/// The main library struct.
pub struct Shtola {
- pub ware: Ware<IR>,
- pub ir: IR,
+ ware: Ware<IR>,
+ ir: IR,
}
impl Shtola {
+ /// Creates a new empty Shtola struct.
pub fn new() -> Shtola {
let config: Config = Default::default();
let ir = IR {
@@ -34,32 +65,63 @@ impl Shtola {
}
}
+ /// Appends glob-matched paths to the ignore list. If a glob path matches, the
+ /// file is excluded from the IR.
+ /// ```
+ /// use shtola::Shtola;
+ ///
+ /// let mut m = Shtola::new();
+ /// m.ignores(&mut vec!["node_modules".into(), "vendor/bundle/".into()])
+ /// ```
pub fn ignores(&mut self, vec: &mut Vec<String>) {
self.ir.config.ignores.append(vec);
self.ir.config.ignores.dedup();
}
+ /// Sets the source directory to read from. Should be relative.
pub fn source<T: Into<PathBuf>>(&mut self, path: T) {
self.ir.config.source = fs::canonicalize(path.into()).unwrap();
}
+ /// Sets the destination path to write to. This directory will be created on
+ /// calling this function if it doesn't exist.
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();
}
+ /// Sets whether the destination directory should be removed before building.
+ /// The removal only happens once calling [`Shtola::build`](#method.build).
+ /// Default is `false`.
pub fn clean(&mut self, b: bool) {
self.ir.config.clean = b;
}
+ /// Sets whether frontmatter should be parsed. Default is `true`.
pub fn frontmatter(&mut self, b: bool) {
self.ir.config.frontmatter = b;
}
+ /// Registers a new plugin function in its middleware chain.
+ ///
+ /// ```
+ /// use shtola::{Shtola, IR};
+ ///
+ /// let mut m = Shtola::new();
+ /// let plugin = Box::new(|ir: IR| ir);
+ /// m.register(plugin);
+ /// ```
pub fn register(&mut self, func: Box<dyn Fn(IR) -> IR>) {
self.ware.wrap(func);
}
+ /// Performs the build process. This does a couple of things:
+ /// - If [`Shtola::clean`](#method.clean) is set, removes and recreates the
+ /// destination directory
+ /// - Reads from the source file and ignores files as it's been configured
+ /// - Parses front matter for the remaining files
+ /// - Runs the middleware chain, executing all plugins
+ /// - Writes the result back to the destination directory
pub fn build(&mut self) -> Result<IR, std::io::Error> {
if self.ir.config.clean {
fs::remove_dir_all(&self.ir.config.destination)?;
@@ -72,34 +134,42 @@ impl Shtola {
builder.add(Glob::new(item).unwrap());
}
let set = builder.build().unwrap();
- let unfiltered_files = read_dir(&self.ir.config.source, self.ir.config.frontmatter)?;
- let files = unfiltered_files.iter().filter(|(p, _)| {
- let path = p.to_str().unwrap();
- !set.is_match(path)
- });
+ let files = read_dir(&self.ir.config.source, self.ir.config.frontmatter, set)?;
- self.ir.files = files.cloned().collect();
+ self.ir.files = files;
let result_ir = self.ware.run(self.ir.clone());
write_dir(result_ir.clone(), &self.ir.config.destination)?;
Ok(result_ir)
}
}
+/// Convenience type to return from plugin functions.
pub type Plugin = Box<dyn Fn(IR) -> IR>;
+/// The intermediate representation that's passed to plugins. Includes global
+/// metadata, the files with frontmatter and the global config.
#[derive(Debug, Clone)]
pub struct IR {
+ /// The filestate, contained in an `im::HashMap`.
pub files: HashMap<PathBuf, ShFile>,
+ /// The configuration.
pub config: Config,
+ /// Global metadata managed as a `HashMap` that keep JSON values as values.
pub metadata: HashMap<String, json::Value>,
}
+/// Configuration struct.
#[derive(Debug, Clone)]
pub struct Config {
+ /// Files that are to be ignored.
pub ignores: Vec<String>,
+ /// Source to read from.
pub source: PathBuf,
+ /// Destination to write to.
pub destination: PathBuf,
+ /// Whether to clean the destination directory.
pub clean: bool,
+ /// Whether to parse frontmatter.
pub frontmatter: bool,
}
@@ -115,13 +185,31 @@ impl Default for Config {
}
}
+/// Shtola's file representation, with frontmatter included.
#[derive(Debug, Clone)]
pub struct ShFile {
+ /// The frontmatter.
pub frontmatter: json::Value,
+ /// The file contents (without frontmatter).
pub content: Vec<u8>,
}
impl ShFile {
+ /// Creates an empty ShFile. Useful for deleting files using
+ /// [`HashMap::difference`](struct.HashMap.html#method.difference):
+ ///
+ /// ```
+ /// use shtola::{Plugin, IR, ShFile, HashMap};
+ /// use std::path::PathBuf;
+ ///
+ /// fn plugin() -> Plugin {
+ /// Box::new(|ir: IR| {
+ /// let mut deletion_hash: HashMap<PathBuf, ShFile> = HashMap::new();
+ /// deletion_hash.insert("deleted-file.md".into(), ShFile::empty());
+ /// IR { files: deletion_hash.difference(ir.files), ..ir }
+ /// })
+ /// }
+ /// ```
pub fn empty() -> ShFile {
ShFile { frontmatter: json!(null), content: Vec::new() }
}
@@ -130,13 +218,18 @@ impl ShFile {
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_map(|e| e.ok())
- .filter(|e| !e.path().is_dir());
+ .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 = String::new();