shtola

ssg in rust
git clone https://tilde.team/~marisa/repo/shtola.git
Log | Files | Refs | LICENSE

commit 4026577fd5ad676e9fc4c26fd72f642ff1ccf23e
parent 43db77e0d043da345265e63c1fec9499bc7d3135
Author: marisa <mokou@posteo.de>
Date:   Tue, 12 Nov 2019 15:46:02 +0100

Document shtola

Diffstat:
Mshtola/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();