diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..8c0e653 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,60 @@ +name: Build and Deploy Docs + +on: + push: + branches: [ "main" ] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + components: rustfmt, clippy + + - name: Cache Cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Build documentation site + run: cargo run -p docs --release + env: + CARGO_NET_GIT_FETCH_WITH_CLI: true + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs/dist + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/Cargo.lock b/Cargo.lock index 4e5a8bf..83eaee6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -135,6 +135,13 @@ version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" +[[package]] +name = "docs" +version = "0.1.0" +dependencies = [ + "librawssg", +] + [[package]] name = "equivalent" version = "1.0.2" diff --git a/Cargo.toml b/Cargo.toml index b4fa58a..743fecc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "3" -members = ["librawssg","librawssg_compiler", "librawssg_config", "librawssg_demo", "librawssg_error", "librawssg_fs","librawssg_handler", "librawssg_templates"] +members = ["librawssg","librawssg_compiler", "librawssg_config", "librawssg_demo", "librawssg_error", "librawssg_fs","librawssg_handler", "librawssg_templates", "docs"] [workspace.lints.clippy] all = { level = "deny", priority = -1 } diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..3e22129 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1 @@ +/dist \ No newline at end of file diff --git a/docs/Cargo.toml b/docs/Cargo.toml new file mode 100644 index 0000000..fa076fb --- /dev/null +++ b/docs/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "docs" +version = "0.1.0" +edition = "2024" + +[dependencies] +librawssg = {version = "1.0.0", path = "../librawssg" } + +[lints] +workspace = true diff --git a/docs/src/content/api/compiler.raw b/docs/src/content/api/compiler.raw new file mode 100644 index 0000000..0f76215 --- /dev/null +++ b/docs/src/content/api/compiler.raw @@ -0,0 +1,1139 @@ +
+ Version: 1.0.0 (implied)
Crate name:
+ librawssg_compiler
Description: Provides
+ the core compilation pipeline for the librawssg static site
+ generator. This crate orchestrates the entire build process: reading content
+ files, processing them via pluggable processors, rendering templates, copying
+ static assets, running custom generators, and outputting the final site
+ atomically.
+
PipelineBuilder
+ new()
+ config()
+ load_config()
+ content_dir()
+ output_dir()
+ with_fs()
+ with_renderer()
+ add_processor()
+ with_context_builder()
+ add_generator()
+ build() Method
+ Default Implementation
+ ContextBuilder Trait
+ build_context()
+ TeraContextBuilder
+ Generator Trait
+ generate()
+ Pipeline Struct
+ config()
+ run()
+
+ librawssg_compiler is the orchestration layer that ties together
+ all other components of the static site generator:
+
Config from
+ librawssg_config defines content rules and paths.
+ Processor from
+ librawssg_handler transforms source files into
+ Document objects.
+ Renderer and
+ RenderContext from
+ librawssg_templates handle template rendering.
+ FileSystem from
+ librawssg_fs abstracts all I/O operations.
+ Generator (defined here) allows custom
+ post‑processing steps.
+ ContextBuilder (defined here) constructs the
+ render context from a Document and Config.
+
+ The main entry point is PipelineBuilder, which constructs a
+ Pipeline after validating the configuration and ensuring
+ mandatory components are present. Running the pipeline performs the full site
+ generation in an atomic fashion, producing output in the configured output
+ directory.
+
+ The crate root (lib.rs) declares the following public modules:
+
builder – Contains PipelineBuilder.context – Contains ContextBuilder trait and
+ TeraContextBuilder.
+ generator – Contains Generator trait.pattern – Contains match_pattern function.pipeline – Contains Pipeline struct.Re‑exported types at the crate root:
+pub use builder::PipelineBuilder;
+pub use context::ContextBuilder;
+pub use context::TeraContextBuilder;
+pub use generator::Generator;
+pub use pipeline::Pipeline;
+
+
+ The match_pattern function is also re‑exported? Actually it is
+ in pattern module and not re‑exported at root, so users must use
+ librawssg_compiler::pattern::match_pattern. However, in
+ pipeline.rs it is imported via
+ crate::pattern::match_pattern, but for external users they need
+ to access it via module path.
+
PipelineBuilder
+ PipelineBuilder is a builder‑style struct that collects all
+ components needed to run the compilation pipeline and then builds a
+ Pipeline.
+
pub struct PipelineBuilder {
+ config: Config,
+ content_dir: PathBuf,
+ output_dir: PathBuf,
+ fs: Box<dyn FileSystem>,
+ renderer: Option<Box<dyn Renderer>>,
+ processors: Vec<Box<dyn Processor>>,
+ context_builder: Option<Box<dyn ContextBuilder>>,
+ generators: Vec<Box<dyn Generator>>,
+}
+
++ Note: Fields are private; use the builder methods to + configure. +
+PipelineBuilder::new#[must_use]
+pub fn new() -> Self
+
+Purpose: Creates a new builder with default values:
+config: Config::default()content_dir: "content"output_dir: "dist"fs: Box::new(RealFs)renderer: Noneprocessors: empty vectorcontext_builder: Nonegenerators: empty vectorReturns: A fresh PipelineBuilder.
Example:
+let builder = PipelineBuilder::new();
+
+
+ All builder methods consume self and return Self,
+ allowing method chaining.
+
config#[must_use]
+pub fn config(mut self, config: Config) -> Self
+
+Purpose: Sets the configuration object.
+Parameters:
+config: A Config instance from
+ librawssg_config.
+ Returns: The builder with the config set.
+load_configpub fn load_config<P: AsRef<Path> + Send + Sync>(mut self, path: P) -> Result<Self>
+
+
+ Purpose: Reads a YAML config file from disk and parses it
+ into a Config. Errors are converted to
+ Error::Config.
+
Parameters:
+path: Path to the YAML file.
+ Returns: Ok(Self) if parsing succeeds,
+ otherwise Err(Error::Config).
+
+ Note: The file is read using standard
+ std::fs::read_to_string; the error is wrapped in
+ Error::Config.
+
content_dir#[must_use]
+pub fn content_dir(mut self, dir: impl Into<PathBuf>) -> Self
+
+Purpose: Overrides the content directory.
+Parameters:
+dir: Any type convertible to PathBuf.
+ Default: "content" (but may be
+ overridden by config if not explicitly set; see build()).
+
output_dir#[must_use]
+pub fn output_dir(mut self, dir: impl Into<PathBuf>) -> Self
+
+Purpose: Overrides the output directory.
+Default: "dist".
with_fs#[must_use]
+pub fn with_fs(mut self, fs: Box<dyn FileSystem>) -> Self
+
++ Purpose: Sets a custom filesystem implementation. Useful for + testing or non‑standard backends. +
+Default: RealFs.
with_renderer#[must_use]
+pub fn with_renderer(mut self, renderer: Box<dyn Renderer>) -> Self
+
+
+ Purpose: Sets the template renderer.
+ Mandatory; build() will fail if not set.
+
add_processor#[must_use]
+pub fn add_processor(mut self, processor: Box<dyn Processor>) -> Self
+
++ Purpose: Adds a content processor to the pipeline. Multiple + processors can be added; they are tried in order for each source file. +
+with_context_builder#[must_use]
+pub fn with_context_builder(mut self, builder: Box<dyn ContextBuilder>) -> Self
+
+
+ Purpose: Sets the context builder.
+ Mandatory; build() will fail if not set.
+
add_generator#[must_use]
+pub fn add_generator(mut self, generator: Box<dyn Generator>) -> Self
+
++ Purpose: Adds a post‑processing generator. Generators run + after all documents are rendered and static assets copied. +
+pub fn build(mut self) -> Result<Pipeline>
+
+
+ Purpose: Validates the configuration, ensures required
+ components are present, and constructs a Pipeline.
+
Behavior:
+self.config.validate()? (see
+ librawssg_config::Config::validate).
+ content_dir is still the default
+ "content" (i.e., not changed by
+ content_dir()), it is replaced with
+ self.config.build.content_dir.
+ output_dir is still
+ "dist", it is replaced with
+ self.config.build.output_dir.
+ self.renderer (using
+ take()). If None, returns
+ Error::Config("template renderer not set").
+ self.context_builder. If
+ None, returns
+ Error::Config("context builder not set").
+ Pipeline and returns
+ Ok.
+ Returns:
+Ok(Pipeline) on success.Err(Error::Validation) if config invalid.Err(Error::Config) if renderer or context builder missing.
+ impl Default for PipelineBuilder {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+Allows creating with PipelineBuilder::default().
use librawssg_compiler::{PipelineBuilder, TeraContextBuilder};
+use librawssg_config::Config;
+use librawssg_fs::RealFs;
+use librawssg_handler::Processor;
+use librawssg_templates::{Renderer, TeraRenderer};
+
+// Assuming custom processor and renderer exist
+let config = Config::new().with_site_name("My Site");
+let processor = Box::new(MyProcessor);
+let renderer = Box::new(TeraRenderer::new()); // TeraRenderer must be configured with templates beforehand
+let context_builder = Box::new(TeraContextBuilder);
+
+let pipeline = PipelineBuilder::new()
+ .config(config)
+ .content_dir("src")
+ .output_dir("public")
+ .with_fs(Box::new(RealFs))
+ .with_renderer(renderer)
+ .with_context_builder(context_builder)
+ .add_processor(processor)
+ .build()?;
+
+ContextBuilderpub trait ContextBuilder: Send + Sync {
+ fn build_context(&self, config: &Config, doc: &Document) -> Result<Box<dyn RenderContext>>;
+}
+
+
+ Purpose: Abstract factory that creates a
+ RenderContext from the global Config and a specific
+ Document. The renderer then uses this context to render the
+ document's template.
+
+ Requirements: Implementors must be Send + Sync.
+
build_contextParameters:
+config: Reference to the site configuration.doc: Reference to the document being rendered.Returns:
+Ok(Box<dyn RenderContext>) – a boxed trait object
+ holding the render context.
+ Err(librawssg_error::Error) if context creation fails.TeraContextBuilder#[derive(Debug, Default, Clone, Copy)]
+pub struct TeraContextBuilder;
+
+
+ A concrete implementation of ContextBuilder that produces a
+ tera::Context populated with common page and site data.
+
+ TeraContextBuilder creates a new tera::Context and
+ inserts the following keys:
+
| Key | +Value Source | +Description | +
|---|---|---|
site |
+ &config.site |
+ The full SiteConfig object. |
+
page_title |
+ &doc.metadata.title |
+ Document title. | +
page_description |
+ &doc.metadata.description |
+ Document description. | +
page_author |
+ &doc.metadata.author |
+ Optional author. | +
page_date |
+ &doc.metadata.date |
+ Optional publication date. | +
page_tags |
+ &doc.metadata.tags |
+ Vector of tags. | +
page_content |
+ &doc.body |
+ The rendered body content of the document. | +
page_url |
+ &doc.url |
+ Relative URL of the document. | +
page_depth |
+ &doc.depth |
+ Depth in the site hierarchy. | +
page_type |
+ &doc.content_type |
+ Content type identifier (e.g., "blog"). |
+
page_is_list |
+ &doc.is_list |
+ Boolean indicating list page. | +
page_list_items |
+ &doc.list_items |
+ Optional vector of child documents for lists. | +
+ Note: The page_list_items field is inserted as
+ &doc.list_items which is
+ Option<Vec<Document>>. In Tera templates, this will
+ be None or an array.
+
Example:
+use librawssg_compiler::TeraContextBuilder;
+let builder = TeraContextBuilder;
+let ctx = builder.build_context(&config, &doc)?;
+// Pass `ctx` to renderer.render(...)
+
+Generatorpub trait Generator: Send + Sync {
+ fn generate(&self, pipeline: &Pipeline, output_base: &Path) -> Result<()>;
+}
+
++ Purpose: Allows custom post‑processing steps after the main + site generation. Generators can write additional files to the output + directory (e.g., RSS feed, sitemap, search index). +
+
+ Requirements: Implementors must be Send + Sync.
+
generateParameters:
+pipeline: Reference to the running Pipeline,
+ which provides access to its configuration, filesystem, etc. (though the
+ fields are crate‑private, the config() method is available).
+ output_base: Path to the temporary output directory where
+ generated files should be written. The pipeline's
+ run() method later moves this directory atomically to the
+ final output location.
+ Returns:
+Ok(()) on success.Err(librawssg_error::Error) on failure.Example (from tests):
+struct DummyGenerator;
+
+impl Generator for DummyGenerator {
+ fn generate(&self, _pipeline: &Pipeline, output_base: &Path) -> Result<()> {
+ let path = output_base.join("generated.txt");
+ std::fs::write(&path, b"generated")?;
+ Ok(())
+ }
+}
+
+match_pattern#[must_use]
+pub fn match_pattern(pattern: &str, path: &Path) -> bool
+
+Location: librawssg_compiler::pattern
+ Purpose: Checks whether a file path matches a glob pattern
+ with support for * (within a segment) and
+ ** (across segments). Used by the pipeline to assign content
+ types based on ContentRule patterns.
+
Parameters:
+pattern: A glob‑like pattern string, e.g.,
+ "blog/**/*.html".
+ path: A Path to test (usually a relative path
+ from the content directory).
+
+ Returns: true if the path matches the pattern;
+ false otherwise.
+
* – Matches any sequence of characters within a single path
+ segment (i.e., does not cross /).
+ ** – Matches any number of path segments, including zero.
+ Limitations:
+* and ** are supported; no character
+ classes ([abc]) or alternation.
+ /; backslashes are not treated as
+ separators (path normalization may be needed on Windows).
+
+ The function first converts the path to a string (lossy) and splits both
+ pattern and path by /. It then calls an internal recursive
+ match_pattern_slice. The logic:
+
true.** segments are allowed.
+ "**":
+ true.segment_matches (handles * wildcards), and
+ recursion continues on the rest.
+ segment_matches handles * by trying to match the
+ remainder of the pattern segment against suffixes of the path segment.
+ use std::path::Path;
+use librawssg_compiler::pattern::match_pattern;
+
+assert!(match_pattern("**/*.html", Path::new("blog/post.html")));
+assert!(match_pattern("blog/**/*.html", Path::new("blog/2024/post.html")));
+assert!(match_pattern("*.html", Path::new("index.html")));
+assert!(!match_pattern("*.html", Path::new("blog/post.html")));
+assert!(match_pattern("**", Path::new("anything/at/all")));
+assert!(match_pattern("**/*.md", Path::new("readme.md"))); // zero segments before .md
+
+Pipeline
+ The Pipeline is the core execution engine. It is created by
+ PipelineBuilder::build() and holds all necessary components.
+
pub struct Pipeline {
+ config: Config,
+ fs: Box<dyn FileSystem>,
+ renderer: Box<dyn Renderer>,
+ processors: Vec<Box<dyn Processor>>,
+ context_builder: Box<dyn ContextBuilder>,
+ generators: Vec<Box<dyn Generator>>,
+ content_dir: PathBuf,
+ output_dir: PathBuf,
+}
+
+
+ All fields are private; access to configuration is provided via the
+ config() method.
+
#[must_use]
+pub const fn config(&self) -> &Config
+
++ Purpose: Returns a reference to the configuration used by + this pipeline. +
+Returns: &Config.
pub fn run(&self) -> Result<()>
+
++ Purpose: Executes the full site generation process + atomically. +
+Behavior:
+output_dir.with_extension("tmp"). For example, if
+ output_dir is "dist", the temp dir is
+ "dist.tmp".
+ generate_to(&tmp_dir) to perform the
+ actual generation into the temporary location.
+ Ok(()).CrossesDevices error (different
+ filesystems), fallback: copy the temp dir contents to the final
+ output dir using copy_dir_all (internal method), then
+ remove the temp dir.
+ Error::Generation("atomic rename failed: ...").
+
+ Returns: Ok(()) on success, or
+ Err(Error).
+
+ Note: This atomic approach ensures that the final output + directory is never left in a partially generated state; either the old output + remains untouched (if generation fails) or the new output replaces it + atomically (or near‑atomically). +
+
+ The internal method
+ generate_to(output_base: &Path) orchestrates the entire
+ generation. The following steps are performed (not public, but described for
+ understanding):
+
fs.create_dir_all(output_base).
+ process_documents() to get a vector of Document.
+ HashMap<String, Vec<Document>>.
+ Document where is_list == false, calls
+ render_document(output_base, doc).
+ ContentRule with list_enabled == true,
+ list_template set, and at least one document, creates a
+ synthetic list document (is_list = true) and renders it using
+ the list template. The list document’s list_items are all
+ documents of that content type. The output path is
+ "{content_type}/index.html".
+ config.build.static_dir exists, its entire contents are
+ copied to output_base/static_dir_name (using
+ copy_dir_all).
+ generators and calls generate() for each,
+ passing the output base.
+
+ process_documents() walks the content directory (using
+ fs.walk_dir). For each file, it:
+
content_dir.can_process(rel, &file_path) returns true is
+ used.
+ process(&*fs, rel, &content_dir), which returns
+ Option<Document>.
+ content_type is overridden
+ based on the matching content rule (via
+ determine_content_type), and its depth is set to
+ the number of path components minus 1.
+
+ If no processor matches, the file is ignored. If a processor returns
+ Ok(None), the file is skipped. If any processor returns an
+ error, the whole processing fails.
+
+ determine_content_type(rel) iterates through the
+ config.content_rules in reverse order (so later
+ rules take precedence) and returns the name of the first rule
+ whose pattern matches the relative path. If no rule matches, the
+ content type defaults to "page".
+
+ render_document calls template_for_document(doc) to
+ get the template name:
+
name equals doc.content_type.
+ doc.is_list and the rule has a list_template,
+ that template is used.
+ template is used.Error::Generation("no content rule found for type
+ '...'").
+ The actual rendering uses:
+context_builder.build_context(&config, doc) to get a
+ RenderContext.
+ renderer.render(template, &*ctx) to produce the HTML
+ string.
+ write_output(output_base, doc, html.as_bytes()) to write the
+ result.
+
+ When generating list pages, the pipeline creates a Metadata with
+ title = content_type and empty description. Then constructs a
+ Document with:
+
body: empty string (the list template is expected to use
+ page_list_items).
+ url: "{content_type}/index.html".
+ output_path:
+ "{content_type}/index.html".
+ source_path:
+ PathBuf::from("__list__") (placeholder).
+ depth: 1.content_type: same as the content type.is_list: true.
+ Then sets list_items to the cloned vector of documents of that
+ type, and renders using the list template.
+
+ The static directory from config.build.static_dir is copied
+ verbatim. The destination is output_base joined with the static
+ directory’s base name (e.g., if static_dir = "static",
+ files are copied to output_base/static/). Directories are
+ recursively copied.
+
+ After all documents and static files are in place, each registered
+ Generator is invoked with &self and
+ output_base. This allows adding custom files like RSS feeds,
+ sitemaps, etc.
+
+ librawssg_compiler uses librawssg_error::Error for
+ all fallible operations. The common error variants encountered:
+
Error::Config – For configuration issues (e.g., missing
+ renderer, invalid config file).
+ Error::Validation – From Config::validate.Error::Generation – For errors during pipeline execution
+ (e.g., write failures, unsafe output path).
+ Error::Io – From filesystem operations (though these may be
+ wrapped in Error::Generation in some places).
+ Error::Render – From template rendering.
+ Methods return Result<T> (alias for
+ std::result::Result<T, librawssg_error::Error>).
+
+ The test file full_compiler_test.rs demonstrates a full working
+ pipeline with mock components. Below is a simplified but complete example.
+
Define mock renderer, context, context builder, and processor:
+use librawssg_compiler::{PipelineBuilder, TeraContextBuilder};
+use librawssg_config::{Config, ContentRule};
+use librawssg_fs::{FileSystem, RealFs};
+use librawssg_handler::{Document, Metadata, Processor};
+use librawssg_templates::{RenderContext, Renderer};
+use std::path::{Path, PathBuf};
+
+// Mock renderer: returns "rendered:{template_name}"
+struct MockRenderer;
+impl Renderer for MockRenderer {
+ fn render(&self, template_name: &str, _ctx: &dyn RenderContext) -> Result<String> {
+ Ok(format!("rendered:{template_name}"))
+ }
+}
+
+// Mock context
+struct MockContext;
+impl RenderContext for MockContext {
+ fn as_any(&self) -> &dyn Any { self }
+ fn as_mut_any(&mut self) -> &mut dyn Any { self }
+}
+
+// Mock context builder
+struct MockContextBuilder;
+impl ContextBuilder for MockContextBuilder {
+ fn build_context(&self, _config: &Config, _doc: &Document) -> Result<Box<dyn RenderContext>> {
+ Ok(Box::new(MockContext))
+ }
+}
+
+// Processor that handles .html files
+struct RawHtmlProcessor;
+impl Processor for RawHtmlProcessor {
+ fn name(&self) -> &'static str { "raw-html" }
+ fn can_process(&self, relative_path: &Path, _original_path: &Path) -> bool {
+ relative_path.extension().is_some_and(|ext| ext == "html")
+ }
+ fn process(&self, fs: &dyn FileSystem, relative_path: &Path, content_dir: &Path) -> Result<Option<Document>> {
+ let full_path = content_dir.join(relative_path);
+ let content = fs.read_to_string(&full_path)?;
+ let url = relative_path.with_extension("html").to_string_lossy().to_string();
+ let output_path = PathBuf::from(&url);
+ let metadata = Metadata::new("Test", "Description")?;
+ let doc = Document::new(
+ metadata,
+ content,
+ url,
+ output_path,
+ relative_path.to_path_buf(),
+ 0,
+ "page".to_string(),
+ false,
+ )?;
+ Ok(Some(doc))
+ }
+}
+
+// Config with one rule
+fn setup_config() -> Config {
+ let mut config = Config::new().with_site_name("Compiler Test");
+ config.add_content_rule(ContentRule::new("page", "**/*.html", "base"));
+ config
+}
+
+// Build pipeline
+fn build_pipeline(content_dir: &Path, output_dir: &Path) -> Result<Pipeline> {
+ PipelineBuilder::new()
+ .config(setup_config())
+ .content_dir(content_dir)
+ .output_dir(output_dir)
+ .with_fs(Box::new(RealFs))
+ .with_renderer(Box::new(MockRenderer))
+ .with_context_builder(Box::new(MockContextBuilder))
+ .add_processor(Box::new(RawHtmlProcessor))
+ .build()
+}
+
+let tmp = tempfile::TempDir::new()?;
+let content_dir = tmp.path().join("content");
+let output_dir = tmp.path().join("dist");
+std::fs::create_dir_all(&content_dir)?;
+std::fs::write(content_dir.join("index.html"), "<h1>Home</h1>")?;
+
+let pipeline = build_pipeline(&content_dir, &output_dir)?;
+pipeline.run()?;
+
+let output_file = output_dir.join("index.html");
+assert!(output_file.exists());
+let content = std::fs::read_to_string(output_file)?;
+assert_eq!(content, "rendered:base");
+
+
+ The test file tests/full_compiler_test.rs contains comprehensive
+ integration tests covering:
+
+ Each test uses temporary directories (tempfile) and mock
+ implementations to isolate components. The tests serve as executable examples
+ of how to configure and run the pipeline.
+
+ librawssg_compiler is the central execution engine of the static
+ site generator. It provides a flexible builder to assemble the necessary
+ components, a robust Pipeline that orchestrates all steps, and
+ extension points via Processor, Renderer,
+ ContextBuilder, and Generator. The pattern matching
+ function and atomic output generation ensure correctness and safety. The
+ comprehensive test suite demonstrates practical usage and edge cases.
+
For further details, refer to the source code and the test file.
diff --git a/docs/src/content/api/config.raw b/docs/src/content/api/config.raw new file mode 100644 index 0000000..c9b0b22 --- /dev/null +++ b/docs/src/content/api/config.raw @@ -0,0 +1,1071 @@ +
+ Version: 1.0.0 (implied)
Crate name:
+ librawssg_config
Description: Defines
+ configuration data structures for the librawssg static site
+ generator. Provides Config, SiteConfig,
+ BuildConfig, ContentRule, and
+ NavItem types, along with serialization/deserialization support
+ (YAML and JSON) and validation logic.
+
BuildConfig
+ new()
+ Default Implementation
+ ContentRule
+ new()
+ Default Implementation
+ NavItem
+ new()
+ Default Implementation
+ SiteConfig
+ new()
+ Default Implementation
+ Config
+ new()
+ with_site_name()
+ add_content_rule()
+ find_rule_by_name()
+ remove_rule_by_name()
+ has_duplicate_rule_names()
+ validate()
+ from_yaml_str()
+ to_yaml_string()
+ from_json_str()
+ to_json_string()
+
+ librawssg_config provides the central configuration types used
+ by the static site generator. The main Config struct combines
+ site settings (SiteConfig), build paths
+ (BuildConfig), content processing rules
+ (ContentRule), and arbitrary extra data. All types are
+ serializable/deserializable via serde, enabling configuration to
+ be read from and written to YAML or JSON files.
+
+ The types are designed with sensible defaults and include a
+ validate() method to ensure the configuration is internally
+ consistent and safe (e.g., preventing path traversal in patterns).
+
+ The crate root (lib.rs) declares the following public modules:
+
build – Contains BuildConfig.config – Contains Config.content_rule – Contains ContentRule.nav – Contains NavItem.site – Contains SiteConfig.All public types are re‑exported at the crate root for convenience:
+pub use build::BuildConfig;
+pub use config::Config;
+pub use content_rule::ContentRule;
+pub use nav::NavItem;
+pub use site::SiteConfig;
+
+BuildConfigRepresents filesystem path configuration for the build process.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[non_exhaustive]
+pub struct BuildConfig {
+ #[serde(default = "default_content_dir")]
+ pub content_dir: String,
+ #[serde(default = "default_output_dir")]
+ pub output_dir: String,
+ #[serde(default = "default_templates_dir")]
+ pub templates_dir: String,
+ #[serde(default = "default_static_dir")]
+ pub static_dir: String,
+}
+
+| Field | +Type | +Default Value | +Description | +
|---|---|---|---|
content_dir |
+ String |
+ "content" |
+ Directory containing source content files. | +
output_dir |
+ String |
+ "dist" |
+ Directory where generated site output will be written. | +
templates_dir |
+ String |
+ "templates" |
+ Directory containing template files. | +
static_dir |
+ String |
+ "static" |
+ Directory containing static assets (copied as-is). | +
+ Note: #[non_exhaustive] prevents external
+ crates from exhaustively matching or constructing with a struct literal. Use
+ the provided constructors or update syntax.
+
#[must_use]
+pub fn new() -> Self
+
+
+ Purpose: Creates a BuildConfig with default
+ values (identical to BuildConfig::default()).
+
+ Returns: A new BuildConfig with all fields set
+ to their defaults.
+
Example:
+let build = BuildConfig::new();
+assert_eq!(build.content_dir, "content");
+
+The Default trait is implemented with the following values:
content_dir: "content"output_dir: "dist"templates_dir: "templates"static_dir: "static"
+ These defaults can be overridden during deserialization; missing fields in
+ serialized data will fall back to these defaults (thanks to
+ #[serde(default = "...")]).
+
+ BuildConfig derives Serialize and
+ Deserialize. When deserializing from YAML/JSON, any omitted
+ fields will use the specified default functions. This allows partial
+ configuration.
+
Example (from tests):
+let yaml = "content_dir: custom_content\noutput_dir: public\n";
+let build: BuildConfig = serde_yaml::from_str(yaml)?;
+assert_eq!(build.content_dir, "custom_content");
+assert_eq!(build.templates_dir, "templates"); // default
+
+ContentRuleDefines how a certain group of content files should be processed.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
+#[non_exhaustive]
+pub struct ContentRule {
+ pub name: String,
+ pub pattern: String,
+ pub template: String,
+ #[serde(default)]
+ pub list_template: Option<String>,
+ #[serde(default)]
+ pub list_enabled: bool,
+ #[serde(default)]
+ pub extra: HashMap<String, serde_json::Value>,
+}
+
+| Field | +Type | +Default | +Description | +
|---|---|---|---|
name |
+ String |
+ "" |
+
+ Unique identifier for the rule (e.g., "blog",
+ "page").
+ |
+
pattern |
+ String |
+ "" |
+
+ Glob pattern matching content files (e.g.,
+ "**/*.md"). Must not contain ...
+ |
+
template |
+ String |
+ "" |
+ Name of the template to use for rendering each matched file. | +
list_template |
+ Option<String> |
+ None |
+ + Optional template name for rendering list pages (e.g., index pages). + | +
list_enabled |
+ bool |
+ false |
+ Whether list generation is enabled for this rule. | +
extra |
+ HashMap<String, serde_json::Value> |
+ empty map | +Arbitrary extra data associated with the rule. | +
#[must_use]
+pub fn new(
+ name: impl Into<String>,
+ pattern: impl Into<String>,
+ template: impl Into<String>,
+) -> Self
+
+
+ Purpose: Creates a ContentRule with the
+ required fields (name, pattern,
+ template). All other fields are set to their defaults.
+
Parameters:
+name: The rule name (converted to String).pattern: The glob pattern (converted to String).
+ template: The template name (converted to
+ String).
+ Returns: A new ContentRule instance.
Example:
+let rule = ContentRule::new("blog", "**/*.md", "post");
+assert_eq!(rule.name, "blog");
+assert!(!rule.list_enabled);
+
+The Default implementation (derived) sets:
name, pattern, template: empty
+ strings
+ list_template: Nonelist_enabled: falseextra: empty map
+ Serializes/deserializes with serde. Missing optional fields
+ default as specified. The extra map can hold any JSON‑compatible
+ values.
+
Example:
+let mut rule = ContentRule::new("page", "**/*.html", "base");
+rule.list_enabled = true;
+rule.list_template = Some("list".into());
+rule.extra.insert("key".into(), json!("value"));
+let yaml = serde_yaml::to_string(&rule)?;
+let parsed: ContentRule = serde_yaml::from_str(&yaml)?;
+assert_eq!(rule, parsed);
+
+NavItemRepresents an item in a navigation menu (navbar or sidebar).
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
+#[non_exhaustive]
+pub struct NavItem {
+ pub label: String,
+ pub url: String,
+ pub children: Vec<Self>,
+}
+
+| Field | +Type | +Default | +Description | +
|---|---|---|---|
label |
+ String |
+ "" |
+ Display text for the navigation link. | +
url |
+ String |
+ "" |
+ URL the link points to (relative or absolute). | +
children |
+ Vec<NavItem> |
+ empty | +Nested sub‑items, enabling hierarchical menus. | +
#[must_use]
+pub fn new(label: impl Into<String>, url: impl Into<String>) -> Self
+
+
+ Purpose: Creates a NavItem with a label and
+ URL. The children vector starts empty.
+
Parameters:
+label: Display label.url: Target URL.Returns: A new NavItem.
Example:
+let item = NavItem::new("Home", "/");
+assert_eq!(item.label, "Home");
+assert!(item.children.is_empty());
+
+
+ NavItem::default() creates an item with empty label, empty URL,
+ and no children.
+
+ Supports serialization and deserialization via serde. Nested
+ children are handled recursively.
+
Example:
+let parent = NavItem::new("Docs", "/docs");
+parent.children.push(NavItem::new("API", "/docs/api"));
+let json = serde_json::to_string(&parent)?;
+let parsed: NavItem = serde_json::from_str(&json)?;
+assert_eq!(parent, parsed);
+
+SiteConfigHolds global site metadata and navigation structures.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[non_exhaustive]
+pub struct SiteConfig {
+ #[serde(default)]
+ pub navbar: Vec<super::NavItem>,
+ #[serde(default)]
+ pub sidebar: Vec<super::NavItem>,
+ #[serde(default = "default_site_name")]
+ pub site_name: String,
+ #[serde(default)]
+ pub description: Option<String>,
+ #[serde(default = "default_language")]
+ pub language: Option<String>,
+ #[serde(default)]
+ pub base_url: Option<String>,
+ #[serde(default)]
+ pub author: Option<String>,
+ #[serde(default)]
+ pub repo_url: Option<String>,
+ #[serde(default)]
+ pub license: Option<String>,
+ #[serde(default)]
+ pub extra: HashMap<String, serde_json::Value>,
+}
+
+| Field | +Type | +Default | +Description | +
|---|---|---|---|
navbar |
+ Vec<NavItem> |
+ empty | +Navigation items for the top bar. | +
sidebar |
+ Vec<NavItem> |
+ empty | +Navigation items for the sidebar. | +
site_name |
+ String |
+ "librawssg" |
+ The name of the website. | +
description |
+ Option<String> |
+ None |
+ Short site description. | +
language |
+ Option<String> |
+ Some("en") |
+
+ Site language code (e.g., "en",
+ "id").
+ |
+
base_url |
+ Option<String> |
+ None |
+
+ Base URL for the site; must start with http:// or
+ https:// if set.
+ |
+
author |
+ Option<String> |
+ None |
+ Default author name. | +
repo_url |
+ Option<String> |
+ None |
+ URL to the source repository. | +
license |
+ Option<String> |
+ None |
+ License identifier (e.g., "MIT"). |
+
extra |
+ HashMap<String, serde_json::Value> |
+ empty map | +Arbitrary extra site‑wide metadata. | +
#[must_use]
+pub fn new(site_name: impl Into<String>) -> Self
+
+
+ Purpose: Creates a SiteConfig with a custom
+ site name. All other fields are set to their defaults (navbar/sidebar empty,
+ language Some("en"), etc.).
+
Parameters:
+site_name: The site name (converted to String).
+ Returns: A new SiteConfig.
Example:
+let site = SiteConfig::new("My Site");
+assert_eq!(site.site_name, "My Site");
+assert_eq!(site.language.as_deref(), Some("en"));
+
+SiteConfig::default() sets:
site_name: "librawssg"language: Some("en")Option fields: None
+ Supports YAML/JSON. Missing fields during deserialization use defaults. The
+ language default is provided by a custom function.
+
Example:
+let mut site = SiteConfig::new("Test");
+site.extra.insert("foo".into(), json!("bar"));
+let yaml = serde_yaml::to_string(&site)?;
+let parsed: SiteConfig = serde_yaml::from_str(&yaml)?;
+assert_eq!(site, parsed);
+
+ConfigThe top‑level configuration combining all other components.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
+#[non_exhaustive]
+pub struct Config {
+ pub site: SiteConfig,
+ pub build: BuildConfig,
+ pub content_rules: Vec<ContentRule>,
+ #[serde(default)]
+ pub extra: HashMap<String, serde_json::Value>,
+}
+
+| Field | +Type | +Default | +Description | +
|---|---|---|---|
site |
+ SiteConfig |
+
+ SiteConfig::default() (site name "librawssg")
+ |
+ Global site configuration. | +
build |
+ BuildConfig |
+ BuildConfig::default() |
+ Build path settings. | +
content_rules |
+ Vec<ContentRule> |
+ empty | +List of content processing rules. | +
extra |
+ HashMap<String, serde_json::Value> |
+ empty map | +Arbitrary top‑level extra data. | +
#[must_use]
+pub fn new() -> Self
+
+
+ Purpose: Creates a Config with all fields
+ defaulted. Equivalent to Config::default().
+
Returns: A new Config.
Example:
+let config = Config::new();
+assert!(config.content_rules.is_empty());
+
+#[must_use]
+pub fn with_site_name(mut self, name: impl Into<String>) -> Self
+
+
+ Purpose: Builder‑style method that sets the
+ site.site_name and returns the modified Config.
+
Parameters:
+name: The new site name.
+ Returns: The same Config with updated site
+ name.
+
Example:
+let config = Config::new().with_site_name("My Awesome Site");
+assert_eq!(config.site.site_name, "My Awesome Site");
+
+add_content_rulepub fn add_content_rule(&mut self, rule: ContentRule)
+
+
+ Purpose: Appends a ContentRule to the
+ content_rules vector.
+
Parameters:
+rule: The rule to add.Example:
+config.add_content_rule(ContentRule::new("blog", "**/*.md", "post"));
+
+find_rule_by_name#[must_use]
+pub fn find_rule_by_name(&self, name: &str) -> Option<&ContentRule>
+
+
+ Purpose: Searches for a content rule by its
+ name field.
+
Parameters:
+name: The rule name to search for.
+ Returns: Some(&ContentRule) if found,
+ otherwise None.
+
Example:
+if let Some(rule) = config.find_rule_by_name("blog") {
+ // ...
+}
+
+remove_rule_by_namepub fn remove_rule_by_name(&mut self, name: &str) -> Option<ContentRule>
+
+
+ Purpose: Removes and returns the first content rule whose
+ name matches the given string.
+
Parameters:
+name: The name of the rule to remove.
+ Returns: Some(ContentRule) if found and
+ removed, otherwise None.
+
Example:
+let removed = config.remove_rule_by_name("blog");
+
+has_duplicate_rule_names#[must_use]
+pub fn has_duplicate_rule_names(&self) -> bool
+
+
+ Purpose: Checks whether any two content rules share the same
+ name.
+
+ Returns: true if duplicates exist,
+ false otherwise.
+
+ Implementation: Uses a HashSet to detect
+ duplicates; O(n) time.
+
Example:
+if config.has_duplicate_rule_names() {
+ // handle error
+}
+
+pub fn validate(&self) -> Result<()>
+
+
+ Purpose: Performs comprehensive validation of the
+ configuration. Returns Ok(()) if the configuration is valid,
+ otherwise an Err(Error::Validation(...)) with a descriptive
+ message.
+
Validation Rules:
+site.site_name must not be empty or whitespace‑only.name must not be empty or whitespace‑only.pattern must not be empty or whitespace‑only.pattern must not contain the substring
+ ".." (to prevent path traversal).
+ template must not be empty or whitespace‑only.site.base_url is Some, it must start with
+ "http://" or "https://".
+ Returns:
+Ok(()) if all checks pass.Err(Error::Validation(message)) on the first failure
+ encountered.
+ Example (from tests):
+let config = valid_config(); // has one rule
+assert!(config.validate().is_ok());
+
+
+ The Config struct can be serialized to and deserialized from
+ YAML and JSON via convenience methods.
+
from_yaml_strpub fn from_yaml_str(yaml: &str) -> Result<Self>
+
+
+ Purpose: Parses a YAML string into a Config.
+
Parameters:
+yaml: YAML content as a string.Returns:
+Ok(Config) on success.Err(Error::Config) if the YAML is invalid (with the
+ underlying serde_yaml error message included).
+ Example:
+let config = Config::from_yaml_str("site:\n site_name: Test\n")?;
+
+to_yaml_stringpub fn to_yaml_string(&self) -> Result<String>
+
+
+ Purpose: Serializes the Config to a YAML
+ string.
+
Returns:
+Ok(String) with YAML representation.Err(Error::Serialization) if serialization fails.from_json_strpub fn from_json_str(json: &str) -> Result<Self>
+
+
+ Purpose: Parses a JSON string into a Config.
+
Parameters:
+json: JSON content as a string.Returns:
+Ok(Config) on success.Err(Error::Config) if the JSON is invalid.to_json_stringpub fn to_json_string(&self) -> Result<String>
+
+
+ Purpose: Serializes the Config to a JSON
+ string.
+
Returns:
+Ok(String) with JSON representation.Err(Error::Serialization) on failure.Example roundtrip:
+let yaml = config.to_yaml_string()?;
+let parsed = Config::from_yaml_str(&yaml)?;
+assert_eq!(config, parsed);
+
+
+ The Config methods and validate use
+ librawssg_error::Result<T> (alias for
+ std::result::Result<T, librawssg_error::Error>). The
+ relevant error variants used in this crate are:
+
Error::Config – For YAML/JSON deserialization failures.Error::Serialization – For serialization failures.Error::Validation – For validate() failures.
+ + All error messages are descriptive and include context (e.g., which rule is + invalid, what condition was violated). +
+
+ All configuration structs derive Serialize and
+ Deserialize from serde. Default values are applied
+ during deserialization for missing fields via
+ #[serde(default = "function")] or
+ #[serde(default)] (which uses
+ Default::default() for the field type).
+
BuildConfig: Each field has a custom default function.ContentRule: Optional fields use
+ #[serde(default)].
+ NavItem: No special defaults; all fields are required in
+ input, but Default is derived for programmatic creation.
+ SiteConfig: site_name has a custom default,
+ language has a custom default returning
+ Some("en"), others use
+ #[serde(default)].
+ Config: site and build are required
+ in YAML/JSON (they don't have #[serde(default)] at the
+ field level, but the struct itself derives Default and the
+ fields are not marked optional; however, when deserializing a top‑level
+ Config, missing site or build will
+ cause an error because they are not optional. In practice, configuration
+ files should include these sections or rely on the
+ Default implementation when constructing programmatically).
+ Important: The Config struct's fields
+ are not marked with #[serde(default)], so during
+ deserialization, missing site or build will
+ cause a parse error. Users must provide at least site and
+ build keys (can be empty maps to get defaults via the inner
+ structs' own defaults). The content_rules and
+ extra fields have #[serde(default)] so they can
+ be omitted.
+ + The test suite provides extensive examples for each type. Below are selected + snippets. +
+let build = BuildConfig::default();
+assert_eq!(build.output_dir, "dist");
+
+let yaml = "content_dir: custom_content\noutput_dir: public\n";
+let build: BuildConfig = serde_yaml::from_str(yaml)?;
+assert_eq!(build.content_dir, "custom_content");
+assert_eq!(build.templates_dir, "templates");
+
+let mut rule = ContentRule::new("page", "**/*.html", "base");
+rule.list_enabled = true;
+rule.list_template = Some("list".into());
+rule.extra.insert("key".into(), json!("value"));
+
+let mut parent = NavItem::new("Docs", "/docs");
+parent.children.push(NavItem::new("API", "/docs/api"));
+
+let site = SiteConfig::new("My Site");
+assert_eq!(site.language.as_deref(), Some("en"));
+
+let mut config = Config::new().with_site_name("My Site");
+config.add_content_rule(ContentRule::new("page", "**/*.html", "base"));
+assert!(config.validate().is_ok());
+
+config.site.base_url = Some("ftp://example.com".to_string());
+assert!(config.validate().is_err());
+
+The crate includes five test files:
+build_tests.rs – Tests BuildConfig defaults,
+ new(), and YAML roundtrip.
+ config_tests.rs – Extensive tests for Config:
+ creation, rule management, validation (all rules), YAML/JSON roundtrip,
+ and error cases.
+ content_rule_tests.rs – Tests
+ ContentRule constructor, defaults, and serialization.
+ nav_tests.rs – Tests NavItem constructor,
+ defaults, children, and serialization.
+ site_tests.rs – Tests SiteConfig constructor,
+ defaults, and serialization.
+
+ All tests are self‑contained and use the must! macro to unwrap
+ results with a helpful message on failure. They serve as executable examples
+ of the API usage.
+
+ librawssg_config provides a clean and extensible configuration
+ system for a static site generator. With sensible defaults, comprehensive
+ validation, and full serde support, it covers the needs of both simple and
+ complex site configurations. The types are designed for ergonomic use and can
+ be easily loaded from YAML or JSON files, making it straightforward to define
+ site‑wide settings, build paths, navigation, and content processing rules.
+
For further details, refer to the source code and test files.
diff --git a/docs/src/content/api/error.raw b/docs/src/content/api/error.raw new file mode 100644 index 0000000..f21a2ab --- /dev/null +++ b/docs/src/content/api/error.raw @@ -0,0 +1,893 @@ +
+ Version: 1.0.0 (implied)
Crate name:
+ librawssg_error
Description: Defines a
+ comprehensive error enum and Result alias for use across the
+ librawssg static site generator ecosystem. The error type is
+ built with thiserror for ergonomic Display and
+ Error implementations, supports source chaining, and is designed
+ to cover all common failure modes in SSG operations.
+
Error Enum
+
+ Io
+ Config
+ Metadata
+ Render
+ Processor
+ Generator
+ PathTraversal
+ MissingConfig
+ Generation
+ NotFound
+ Serialization
+ Validation
+ Duplicate
+ InvalidState
+ Internal
+ Result<T> Type Alias
+ std::error::Error
+ std::io::Error
+ ? Operator
+
+ The librawssg_error crate provides a single, unified error type
+ for the entire static site generator ecosystem. Instead of having each module
+ define its own error types, they all share this Error enum,
+ which categorizes failures into well‑defined variants. The enum is derived
+ with thiserror::Error, giving each variant an automatic
+ Display implementation based on a custom message pattern, and an
+ automatic std::error::Error implementation that preserves source
+ chains when applicable.
+
+ The crate also exports a Result<T> type alias, simplifying
+ function signatures throughout the codebase.
+
Key features:
+Metadata variant can
+ wrap an underlying error (e.g., a YAML parsing error) and expose it via
+ source().
+ From<std::io::Error> allows using the
+ ? operator directly in functions returning
+ Result<T, Error>.
+ #[non_exhaustive], enabling future additions without breaking
+ downstream code.
+ thiserror – Provides the #[derive(Error)] macro
+ that generates Display and Error implementations
+ from the attributes.
+ core::error::Error (or std::error::Error) – Used
+ as a trait object for the source field in the
+ Metadata variant.
+ No other external crates are required.
+Error Enumuse core::error::Error as CoreError;
+use std::path::PathBuf;
+use thiserror::Error;
+
+#[derive(Debug, Error)]
+#[non_exhaustive]
+pub enum Error {
+ #[error("I/O error: {0}")]
+ Io(#[from] std::io::Error),
+
+ #[error("Configuration error: {0}")]
+ Config(String),
+
+ #[error("Failed to parse metadata in {path}")]
+ Metadata {
+ path: PathBuf,
+ #[source]
+ source: Box<dyn CoreError + Send + Sync>,
+ },
+
+ #[error("Template rendering error: {0}")]
+ Render(String),
+
+ #[error("Content processor error: {0}")]
+ Processor(String),
+
+ #[error("Generator error: {0}")]
+ Generator(String),
+
+ #[error("Path traversal attempt detected: {0}")]
+ PathTraversal(String),
+
+ #[error("Missing configuration key: {0}")]
+ MissingConfig(String),
+
+ #[error("Site generation error: {0}")]
+ Generation(String),
+
+ #[error("Resource not found: {0}")]
+ NotFound(String),
+
+ #[error("Serialization error: {0}")]
+ Serialization(String),
+
+ #[error("Validation error: {0}")]
+ Validation(String),
+
+ #[error("Duplicate value: {0}")]
+ Duplicate(String),
+
+ #[error("Invalid state: {0}")]
+ InvalidState(String),
+
+ #[error("Internal error: {0}")]
+ Internal(String),
+}
+
+#[derive(Debug, Error)] – Derives
+ Debug and std::error::Error. The
+ Error derive from thiserror also generates a
+ Display implementation based on the
+ #[error("...")] attributes.
+ #[non_exhaustive] – Indicates that the enum
+ may gain new variants in future releases. Downstream crates must not
+ exhaustively match on this enum; they must include a wildcard arm
+ (_) when matching.
+ Because the enum is non‑exhaustive, external code cannot write:
+match err {
+ Error::Io(_) => ...,
+ Error::Config(_) => ...,
+ // all variants...
+}
+
+without including a catch‑all arm:
+match err {
+ Error::Io(_) => ...,
+ Error::Config(_) => ...,
+ // ...
+ _ => { /* handle unknown future variants */ }
+}
+
+This ensures forward compatibility.
+Io#[error("I/O error: {0}")]
+Io(#[from] std::io::Error),
+
+std::io::Error.
+ "I/O error: {underlying_io_error_message}".
+ #[from]: Automatically provides
+ From<std::io::Error> for Error, allowing the
+ ? operator in functions returning
+ Result<T, Error>.
+ source() returns
+ Some(&io_error) because
+ std::io::Error implements std::error::Error.
+ Example:
+let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
+let err = Error::Io(io_err);
+assert_eq!(err.to_string(), "I/O error: file missing");
+
+Config#[error("Configuration error: {0}")]
+Config(String),
+
+String containing a human‑readable
+ description.
+ "Configuration error: {message}".
+ None (no underlying error is
+ stored).
+ Example:
+let err = Error::Config("invalid YAML".to_string());
+assert_eq!(err.to_string(), "Configuration error: invalid YAML");
+
+Metadata#[error("Failed to parse metadata in {path}")]
+Metadata {
+ path: PathBuf,
+ #[source]
+ source: Box<dyn CoreError + Send + Sync>,
+},
+
+path: PathBuf – The path to the source file where
+ metadata parsing failed.
+ source: Box<dyn CoreError + Send + Sync> – The
+ underlying error that caused the failure (boxed trait object).
+ "Failed to parse metadata in {path}". The
+ {path} placeholder prints the PathBuf using its
+ Display implementation.
+ source() returns
+ Some(&*source) (the boxed error as a
+ &dyn Error), enabling error chain inspection.
+ #[source] attribute tells
+ thiserror to use this field as the error source. The
+ Box<dyn CoreError + Send + Sync> allows storing any
+ error type that is Send + Sync.
+ Example:
+use std::path::PathBuf;
+
+let source: Box<dyn core::error::Error + Send + Sync> =
+ Box::new(std::io::Error::other("bad yaml"));
+let err = Error::Metadata {
+ path: PathBuf::from("content/post.md"),
+ source,
+};
+
+assert_eq!(err.to_string(), "Failed to parse metadata in content/post.md");
+let as_core_error: &dyn core::error::Error = &err;
+let source_ref = as_core_error.source().unwrap();
+assert_eq!(source_ref.to_string(), "bad yaml");
+
+Render#[error("Template rendering error: {0}")]
+Render(String),
+
+String describing the rendering
+ problem.
+ "Template rendering error: {message}".
+ None.Example:
+let err = Error::Render("template not found".to_string());
+assert_eq!(err.to_string(), "Template rendering error: template not found");
+
+Processor#[error("Content processor error: {0}")]
+Processor(String),
+
+String with details about the
+ processor failure.
+ "Content processor error: {message}".
+ None.Example:
+let err = Error::Processor("custom processor failed".to_string());
+assert_eq!(err.to_string(), "Content processor error: custom processor failed");
+
+Generator#[error("Generator error: {0}")]
+Generator(String),
+
+String describing the generator
+ error.
+ "Generator error: {message}".
+ None.Example:
+let err = Error::Generator("RSS generation failed".to_string());
+assert_eq!(err.to_string(), "Generator error: RSS generation failed");
+
+PathTraversal#[error("Path traversal attempt detected: {0}")]
+PathTraversal(String),
+
+String containing the offending
+ path or description.
+ "Path traversal attempt detected: {message}".
+ None.Example:
+let err = Error::PathTraversal("../escape".to_string());
+assert_eq!(err.to_string(), "Path traversal attempt detected: ../escape");
+
+MissingConfig#[error("Missing configuration key: {0}")]
+MissingConfig(String),
+
+String naming the missing key.
+ "Missing configuration key: {key}".
+ None.Example:
+let err = Error::MissingConfig("base_url".to_string());
+assert_eq!(err.to_string(), "Missing configuration key: base_url");
+
+Generation#[error("Site generation error: {0}")]
+Generation(String),
+
+String with more information.
+ "Site generation error: {message}".
+ None.Example:
+let err = Error::Generation("output write failed".to_string());
+assert_eq!(err.to_string(), "Site generation error: output write failed");
+
+NotFound#[error("Resource not found: {0}")]
+NotFound(String),
+
+String identifying the missing
+ resource.
+ "Resource not found: {resource}".
+ None.Example:
+let err = Error::NotFound("asset.css".to_string());
+assert_eq!(err.to_string(), "Resource not found: asset.css");
+
+Serialization#[error("Serialization error: {0}")]
+Serialization(String),
+
+String describing the
+ serialization problem.
+ "Serialization error: {message}".
+ None.Example:
+let err = Error::Serialization("invalid JSON".to_string());
+assert_eq!(err.to_string(), "Serialization error: invalid JSON");
+
+Validation#[error("Validation error: {0}")]
+Validation(String),
+
+String explaining what failed
+ validation.
+ "Validation error: {message}".
+ None.Example:
+let err = Error::Validation("name too long".to_string());
+assert_eq!(err.to_string(), "Validation error: name too long");
+
+Duplicate#[error("Duplicate value: {0}")]
+Duplicate(String),
+
+String identifying the duplicated
+ item.
+ "Duplicate value: {message}".
+ None.Example:
+let err = Error::Duplicate("duplicate key".to_string());
+assert_eq!(err.to_string(), "Duplicate value: duplicate key");
+
+InvalidState#[error("Invalid state: {0}")]
+InvalidState(String),
+
+String describing the invalid
+ state.
+ "Invalid state: {message}".
+ None.Example:
+let err = Error::InvalidState("unexpected null".to_string());
+assert_eq!(err.to_string(), "Invalid state: unexpected null");
+
+Internal#[error("Internal error: {0}")]
+Internal(String),
+
+String with details suitable for
+ debugging.
+ "Internal error: {message}".
+ None.Example:
+let err = Error::Internal("bug in code".to_string());
+assert_eq!(err.to_string(), "Internal error: bug in code");
+
+Result<T> Type Aliaspub type Result<T> = core::result::Result<T, Error>;
+
+Result<T> instead of the more verbose
+ std::result::Result<T, librawssg_error::Error>.
+ librawssg ecosystem,
+ functions that may fail with any of the above errors use this alias.
+ Example:
+fn read_config(path: &str) -> librawssg_error::Result<String> {
+ let content = std::fs::read_to_string(path)?; // `?` converts io::Error into Error::Io
+ Ok(content)
+}
+
+std::error::Error
+
+ All variants of Error implement
+ std::error::Error (via thiserror). The
+ source() method returns:
+
Io: Some(&self.0) (the underlying
+ io::Error).
+ Metadata: Some(self.source.as_ref()) (the
+ boxed error).
+ None.
+ This allows error chains to be inspected using
+ std::error::Error::source().
+
Example (from integration tests):
+let source: Box<dyn core::error::Error + Send + Sync> =
+ Box::new(std::io::Error::other("bad yaml"));
+let err = Error::Metadata {
+ path: PathBuf::from("content/post.md"),
+ source,
+};
+
+let as_core_error: &dyn core::error::Error = &err;
+let source_ref = as_core_error.source().unwrap();
+assert_eq!(source_ref.to_string(), "bad yaml");
+
+std::io::Error
+
+ The Io variant has the #[from] attribute, which
+ automatically generates:
+
impl From<std::io::Error> for Error {
+ fn from(err: std::io::Error) -> Error {
+ Error::Io(err)
+ }
+}
+
+
+ This enables the ? operator to convert
+ std::io::Error into Error in any function returning
+ Result<T, Error> (or
+ librawssg_error::Result<T>).
+
Example:
+fn read_file(path: &str) -> Result<String> {
+ let content = std::fs::read_to_string(path)?; // io::Error becomes Error::Io
+ Ok(content)
+}
+
++ The test suite provides excellent examples of how to construct and use the + error type. +
+
+ Each variant has a test asserting its exact Display output. For
+ example:
+
#[test]
+fn display_for_config_error() {
+ let err = Error::Config("invalid YAML".to_string());
+ assert_eq!(err.to_string(), "Configuration error: invalid YAML");
+}
+
+All 15 variants have similar tests in unit_tests.rs.
? Operator
+ The integration test
+ io_error_propagates_via_question_mark demonstrates how
+ ? works:
+
fn read_file(path: &str) -> Result<String> {
+ let content = std::fs::read_to_string(path)?;
+ Ok(content)
+}
+
+#[test]
+fn io_error_propagates_via_question_mark() {
+ let result = read_file("definitely_not_exists.txt");
+ assert!(result.is_err());
+ assert!(matches!(result, Err(Error::Io(_))));
+}
+
+
+ The metadata_error_can_hold_boxed_dyn_error test shows how to
+ store an arbitrary error and retrieve it via source():
+
let source: Box<dyn core::error::Error + Send + Sync> =
+ Box::new(std::io::Error::other("bad yaml"));
+let err = Error::Metadata {
+ path: PathBuf::from("content/post.md"),
+ source,
+};
+
+let as_core_error: &dyn core::error::Error = &err;
+let source_ref = as_core_error.source();
+assert!(source_ref.is_some());
+
++ Property tests verify that the error message always preserves the input + string exactly, regardless of content (including empty strings, newlines, + special characters): +
+#[test]
+fn config_error_message_preserves_input() {
+ let samples = [
+ "",
+ "short",
+ "a very long error message with symbols !@#$%^&*()",
+ "line1\nline2",
+ ];
+
+ for sample in samples {
+ let err = Error::Config(sample.to_string());
+ assert_eq!(err.to_string(), format!("Configuration error: {sample}"));
+ }
+}
+
+
+ Similar tests exist for PathTraversal and Render.
+
+ When writing code in the librawssg ecosystem, follow these
+ recommendations:
+
Error::Io.Error::NotFound.Error::Validation.Error::PathTraversal.
+ Metadata variant (or add a new variant
+ with a #[source] field) to maintain the error chain.
+ Error –
+ Because the enum is non‑exhaustive, always include a catch‑all arm when
+ matching to prevent future breakage.
+ Result<T> alias for concise
+ function signatures.
+ The crate includes three test files:
+unit_tests.rs – Tests each variant’s
+ Display message, source() for
+ Metadata, From<io::Error> conversion, the
+ Result alias, and Debug output.
+ integration_tests.rs – Tests the
+ ? operator integration and the source chain for
+ Metadata using a boxed dynamic error.
+ property_tests.rs – Property‑based tests
+ that verify error messages preserve arbitrary input strings for
+ Config, PathTraversal, and Render.
+ + Together, these tests ensure the error type is robust, easy to use, and + consistent. +
+
+ librawssg_error provides a centralized, well‑structured error
+ type for the entire static site generator project. With 15 descriptive
+ variants, automatic Display and
+ Error implementations, convenient conversion from
+ io::Error, and support for error sources, it simplifies error
+ handling across all modules. The non‑exhaustive design guarantees future
+ extensibility without breaking downstream code.
+
For further details, refer to the source code and test files.
diff --git a/docs/src/content/api/fecade.raw b/docs/src/content/api/fecade.raw new file mode 100644 index 0000000..7e4a6ac --- /dev/null +++ b/docs/src/content/api/fecade.raw @@ -0,0 +1,1198 @@ +A modular static site generator library for Rust.
+
+ This facade crate re‑exports the essential building blocks from the
+ librawssg ecosystem, providing a single convenient entry point
+ for building static site generators. It aggregates configuration management,
+ filesystem abstraction, content processing, template rendering (with Tera
+ built‑in), and build pipeline orchestration.
+
+ librawssg is the top‑level crate that brings together six
+ specialized crates:
+
librawssg_config – Configuration data
+ structures and validation.
+ librawssg_fs – Trait‑based filesystem
+ abstraction with path traversal protection.
+ librawssg_handler – Core document and
+ metadata types, plus the Processor trait.
+ librawssg_templates – Rendering traits
+ (Renderer, RenderContext) and a Tera
+ implementation.
+ librawssg_compiler – Build pipeline
+ orchestration.
+ librawssg_error – Unified error enum and
+ Result alias.
+
+ By depending on librawssg, you get all these components without
+ needing to specify each one individually. The facade also re‑exports the most
+ commonly used types at the crate root for ergonomic access.
+
Add librawssg to your Cargo.toml:
[dependencies]
+librawssg = "1.0.0"
+
+
+ If you are working in the same workspace as the
+ librawssg source, you can use a path dependency:
+
[dependencies]
+librawssg = { path = "../librawssg" }
+
+The crate is compatible with Rust edition 2024 and later.
+The crate organises its re‑exports into submodules for clarity:
+librawssg::config – Configuration types
+ (Config, SiteConfig, BuildConfig,
+ ContentRule, NavItem).
+ librawssg::fs – Filesystem trait and
+ RealFs.
+ librawssg::handler – Document, metadata, and
+ processor contracts.
+ librawssg::templates – Rendering traits and
+ TeraRenderer.
+ librawssg::compiler – Pipeline builder,
+ pipeline, context builders, generators.
+ librawssg::error – Error type and
+ Result alias.
+ + Additionally, the most important types are also re‑exported directly at the + crate root for convenience. +
+
+ The configuration system revolves around the Config struct,
+ which contains site settings, build paths, and content processing rules.
+
+ Config – Top‑level configuration.
+
site: SiteConfig,
+ build: BuildConfig,
+ content_rules: Vec<ContentRule>,
+ extra: HashMap<String, serde_json::Value>.
+ Config::new() -> ConfigConfig::default() -> Configwith_site_name(name: impl Into<String>) -> Self
+ add_content_rule(&mut self, rule: ContentRule)
+ find_rule_by_name(&self, name: &str) ->
+ Option<&ContentRule>
+ remove_rule_by_name(&mut self, name: &str) ->
+ Option<ContentRule>
+ has_duplicate_rule_names(&self) -> bool
+ validate(&self) -> Result<()>
+ from_yaml_str(yaml: &str) ->
+ Result<Self>
+ to_yaml_string(&self) -> Result<String>
+ from_json_str(json: &str) ->
+ Result<Self>
+ to_json_string(&self) -> Result<String>
+
+ SiteConfig – Global site metadata.
+
navbar, sidebar,
+ site_name, description,
+ language, base_url, author,
+ repo_url, license, extra.
+ SiteConfig::new(site_name: impl Into<String>) ->
+ Self, SiteConfig::default().
+ site_name = "librawssg",
+ language = Some("en").
+
+ BuildConfig – Filesystem path settings.
+
content_dir,
+ output_dir, templates_dir,
+ static_dir.
+ BuildConfig::new(),
+ BuildConfig::default().
+ "content",
+ "dist", "templates",
+ "static".
+
+ ContentRule – Defines how a group of
+ files should be processed.
+
name, pattern,
+ template,
+ list_template: Option<String>,
+ list_enabled: bool,
+ extra: HashMap<String, serde_json::Value>.
+ ContentRule::new(name, pattern, template) -> Self.
+ list_enabled = false.
+
+ NavItem – Navigation menu entry.
+
label: String,
+ url: String, children: Vec<NavItem>.
+ NavItem::new(label, url) -> Self.
+
+ FileSystem trait – Abstract filesystem
+ operations. All methods return io::Result or
+ bool.
+
read_to_string, read_bytes,
+ write, create_dir_all,
+ remove_dir_all, remove_file,
+ create_dir, exists,
+ is_dir, is_file,
+ read_dir, copy_file,
+ copy_dir_all, walk_dir,
+ canonicalize, rename,
+ atomic_write, touch,
+ metadata, symlink_metadata,
+ permissions, set_permissions,
+ read_link, hard_link.
+ is_symlinkcanonicalize_or_joinsafe_join –
+ Important for security: ensures the resulting
+ path stays within a base directory.
+ copyrename_or_copy
+ RealFs – Zero‑sized struct implementing
+ FileSystem using std::fs and
+ walkdir.
+
+ Document – Represents a processed content
+ item.
+
metadata: Metadata,
+ body: String, url: String,
+ output_path: PathBuf,
+ source_path: PathBuf, depth: usize,
+ content_type: String, is_list: bool,
+ list_items: Option<Vec<Document>>,
+ taxonomies: HashMap<String, Vec<String>>.
+ Document::new(metadata, body, url, output_path, source_path,
+ depth, content_type, is_list) -> Result<Self>.
+ relative_url(&self) -> &stradd_taxonomy(&mut self, name: impl Into<String>,
+ items: Vec<String>)
+ depth(&self) -> usizewith_list_items(self, items: Vec<Self>) ->
+ Self
+
+ Metadata – Front matter data.
+
title,
+ description, author: Option<String>,
+ repo_url, license,
+ date: Option<NaiveDate>,
+ updated: Option<NaiveDate>,
+ tags: Vec<String>, draft: bool,
+ extra: HashMap<String, serde_json::Value>.
+ Metadata::new(title, description) -> Result<Self>.
+ is_draft(&self) -> boolinsert_extra(&mut self, key, value)get_extra(&self, key: &str) ->
+ Option<&serde_json::Value>
+
+ Processor trait – Interface for
+ transforming source files into Documents.
+
name(&self) -> &strcan_process(&self, relative_path: &Path,
+ original_path: &Path) -> bool
+ process(&self, fs: &dyn FileSystem, relative_path:
+ &Path, content_dir: &Path) ->
+ Result<Option<Document>>
+ priority(&self) -> i32 (default
+ 0).
+
+ RenderContext trait – Type‑erased context
+ for renderers.
+
as_any(&self) -> &dyn Any,
+ as_mut_any(&mut self) -> &mut dyn Any.
+
+ Renderer trait – Interface for template
+ rendering.
+
render(&self, template_name: &str, context: &dyn
+ RenderContext) -> Result<String>.
+
+ TeraRenderer – Concrete renderer using
+ the Tera template engine.
+
tera feature is enabled (enabled by
+ default).
+ TeraRenderer::new(),
+ Default.
+ add_raw_template(&mut self, name: &str, content:
+ &str) -> Result<()>
+ add_template_file(&mut self, path: &Path) ->
+ Result<()>
+ add_template_files_from_dir(&mut self, dir: &Path)
+ -> Result<()>
+ load_templates_dir(&mut self, dir: &Path) ->
+ Result<()>
+ enable_autoescape(&mut self)render_str(&self, template_str: &str, context:
+ &dyn RenderContext) -> Result<String>
+ as_tera(&self) -> &tera::Teraas_tera_mut(&mut self) -> &mut tera::Tera
+
+ PipelineBuilder – Builds a
+ Pipeline.
+
PipelineBuilder::new().
+ config,
+ load_config, content_dir,
+ output_dir, with_fs,
+ with_renderer, add_processor,
+ with_context_builder, add_generator.
+ build(self) -> Result<Pipeline>.
+
+ Pipeline – Executes the site generation.
+
run(&self) -> Result<()>.
+ config(&self) -> &Config.
+
+ ContextBuilder trait – Creates a
+ RenderContext from Config and
+ Document.
+
build_context(&self, config: &Config, doc:
+ &Document) -> Result<Box<dyn
+ RenderContext>>.
+
+ TeraContextBuilder – Default
+ implementation that produces a tera::Context with common
+ page and site variables.
+
+ Generator trait – Custom post‑processing
+ step.
+
generate(&self, pipeline: &Pipeline, output_base:
+ &Path) -> Result<()>.
+
+ match_pattern function (in
+ compiler::pattern) – Glob matching for content rule
+ patterns.
+
Error enum – Variants: Io,
+ Config, Metadata, Render,
+ Processor, Generator,
+ PathTraversal, MissingConfig,
+ Generation, NotFound,
+ Serialization, Validation,
+ Duplicate, InvalidState, Internal.
+ Result<T> – Alias for
+ core::result::Result<T, Error>.
+ + Here’s a minimal but complete example that builds a site from raw HTML + fragments using a custom processor, Tera templates, and the default context + builder: +
+use librawssg::{
+ Config, ContentRule, Document, FileSystem, Metadata, PipelineBuilder, Processor,
+ RealFs, RenderContext, Renderer, TeraContextBuilder, TeraRenderer,
+};
+use std::path::{Path, PathBuf};
+
+// 1. Define a simple processor for .raw files
+struct RawProcessor;
+impl Processor for RawProcessor {
+ fn name(&self) -> &'static str { "raw" }
+ fn can_process(&self, rel: &Path, _orig: &Path) -> bool {
+ rel.extension().and_then(|e| e.to_str()) == Some("raw")
+ }
+ fn process(
+ &self,
+ fs: &dyn FileSystem,
+ rel: &Path,
+ content_dir: &Path,
+ ) -> librawssg::Result<Option<Document>> {
+ let full_path = content_dir.join(rel);
+ let body = fs.read_to_string(&full_path)?;
+ let title = rel.file_stem().unwrap_or_default().to_string_lossy().to_string();
+ let meta = Metadata::new(title, String::new())?;
+ let url = rel.with_extension("html").to_string_lossy().to_string();
+ let output = PathBuf::from(&url);
+ let doc = Document::new(meta, body, url, output, rel.to_path_buf(), 0, "page".to_string(), false)?;
+ Ok(Some(doc))
+ }
+}
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ // 2. Configure the site
+ let mut config = Config::new().with_site_name("My Site");
+ config.add_content_rule(ContentRule::new("page", "**/*.raw", "base.tera"));
+ config.build.content_dir = "content".to_string();
+ config.build.output_dir = "dist".to_string();
+ config.build.static_dir = "static".to_string();
+
+ // 3. Set up the renderer and load templates
+ let mut renderer = TeraRenderer::new();
+ renderer.load_templates_dir(Path::new("templates"))?;
+
+ // 4. Build the pipeline
+ let pipeline = PipelineBuilder::new()
+ .config(config)
+ .content_dir("content")
+ .output_dir("dist")
+ .with_fs(Box::new(RealFs))
+ .with_renderer(Box::new(renderer))
+ .with_context_builder(Box::new(TeraContextBuilder))
+ .add_processor(Box::new(RawProcessor))
+ .build()?;
+
+ // 5. Run the generation
+ pipeline.run()?;
+ println!("Site generated successfully!");
+ Ok(())
+}
+
+
+ For more detailed examples, see the librawssg_demo crate in the
+ repository.
+
+ This section provides a concise reference for every public item re‑exported
+ by librawssg. For deeper details, consult the respective
+ sub‑crate documentation (e.g., librawssg_config,
+ librawssg_compiler).
+
+ All configuration types are in librawssg::config (and
+ re‑exported at root).
+
+ Config
+
new() -> Selfdefault() -> Selfwith_site_name(self, name: impl Into<String>) ->
+ Self
+ add_content_rule(&mut self, rule: ContentRule)
+ find_rule_by_name(&self, name: &str) ->
+ Option<&ContentRule>
+ remove_rule_by_name(&mut self, name: &str) ->
+ Option<ContentRule>
+ has_duplicate_rule_names(&self) -> boolvalidate(&self) -> Result<()>from_yaml_str(yaml: &str) -> Result<Self>
+ to_yaml_string(&self) -> Result<String>
+ from_json_str(json: &str) -> Result<Self>
+ to_json_string(&self) -> Result<String>
+
+ SiteConfig
+
new(site_name: impl Into<String>) -> Self
+ default() -> Selfnavbar: Vec<NavItem>,
+ sidebar: Vec<NavItem>,
+ site_name: String,
+ description: Option<String>,
+ language: Option<String>,
+ base_url: Option<String>,
+ author: Option<String>,
+ repo_url: Option<String>,
+ license: Option<String>,
+ extra: HashMap<String, serde_json::Value>
+
+ BuildConfig
+
new() -> Selfdefault() -> Selfcontent_dir: String,
+ output_dir: String, templates_dir: String,
+ static_dir: String
+
+ ContentRule
+
new(name: impl Into<String>, pattern: impl
+ Into<String>, template: impl Into<String>) ->
+ Self
+ default() -> Selfname: String, pattern: String,
+ template: String,
+ list_template: Option<String>,
+ list_enabled: bool,
+ extra: HashMap<String, serde_json::Value>
+
+ NavItem
+
new(label: impl Into<String>, url: impl
+ Into<String>) -> Self
+ default() -> Selflabel: String, url: String,
+ children: Vec<NavItem>
+
+ FileSystem trait (in
+ librawssg::fs, re‑exported at root)
+
is_symlink,
+ canonicalize_or_join, safe_join,
+ copy, rename_or_copy.
+
+ RealFs (in librawssg::fs,
+ re‑exported at root)
+
FileSystem using std::fs.RealFs (unit struct), RealFs::default().
+
+ Document (in
+ librawssg::handler, re‑exported at root)
+
new(metadata, body, url, output_path, source_path, depth,
+ content_type, is_list) -> Result<Self>
+ relative_url(&self) -> &stradd_taxonomy(&mut self, name, items)depth(&self) -> usizewith_list_items(self, items: Vec<Self>) -> Self
+
+ Metadata
+
new(title, description) -> Result<Self>is_draft(&self) -> boolinsert_extra(&mut self, key, value)get_extra(&self, key: &str) ->
+ Option<&serde_json::Value>
+
+ Processor trait
+
name, can_process,
+ process
+ priority
+ RenderContext trait
+
as_any(&self) -> &dyn Anyas_mut_any(&mut self) -> &mut dyn Any
+ Renderer trait
+
render(&self, template_name: &str, context: &dyn
+ RenderContext) -> Result<String>
+
+ TeraRenderer (feature tera,
+ enabled by default)
+
new() -> Selfadd_raw_template(&mut self, name: &str, content:
+ &str) -> Result<()>
+ add_template_file(&mut self, path: &Path) ->
+ Result<()>
+ add_template_files_from_dir(&mut self, dir: &Path) ->
+ Result<()>
+ load_templates_dir(&mut self, dir: &Path) ->
+ Result<()>
+ enable_autoescape(&mut self)render_str(&self, template_str: &str, context: &dyn
+ RenderContext) -> Result<String>
+ as_tera(&self) -> &tera::Teraas_tera_mut(&mut self) -> &mut tera::Tera
+ Renderer and Default.
+ PipelineBuilder
+
new() -> Selfconfig(self, config: Config) -> Selfload_config<P: AsRef<Path> + Send + Sync>(self,
+ path: P) -> Result<Self>
+ content_dir(self, dir: impl Into<PathBuf>) ->
+ Self
+ output_dir(self, dir: impl Into<PathBuf>) -> Self
+ with_fs(self, fs: Box<dyn FileSystem>) -> Self
+ with_renderer(self, renderer: Box<dyn Renderer>) ->
+ Self
+ add_processor(self, processor: Box<dyn Processor>) ->
+ Self
+ with_context_builder(self, builder: Box<dyn
+ ContextBuilder>) -> Self
+ add_generator(self, generator: Box<dyn Generator>) ->
+ Self
+ build(self) -> Result<Pipeline>
+ Pipeline
+
run(&self) -> Result<()>config(&self) -> &Config
+ ContextBuilder trait
+
build_context(&self, config: &Config, doc:
+ &Document) -> Result<Box<dyn
+ RenderContext>>
+
+ TeraContextBuilder (unit struct)
+
ContextBuilder.TeraContextBuilder (no fields), Default.
+
+ Generator trait
+
generate(&self, pipeline: &Pipeline, output_base:
+ &Path) -> Result<()>
+
+ match_pattern function (accessible via
+ librawssg::compiler::pattern::match_pattern)
+
pub fn match_pattern(pattern: &str, path: &Path) ->
+ bool
+ * and **.
+
+ Error enum (in
+ librawssg::error, re‑exported at root)
+
Display, Debug,
+ std::error::Error.
+ From<std::io::Error> is implemented.
+ Result<T> type alias
+
pub type Result<T> = core::result::Result<T,
+ Error>
+
+ The tera feature is enabled by default and provides the
+ TeraRenderer implementation. To disable it (e.g., if you use a
+ different template engine), set default-features = false in your
+ Cargo.toml:
+
[dependencies]
+librawssg = { version = "1.0.0", default-features = false }
+
+
+ Without this feature, the crate still exports the core traits
+ (Renderer, RenderContext) and all other
+ functionality, but TeraRenderer and
+ TeraContextBuilder are unavailable.
+
+ This project is licensed under the MIT License. See the + LICENSE file for details. +
+
+ This documentation is generated from the source code of the
+ librawssg facade crate and its sub‑crates.
+
+ Version: 1.0.0 (implied)
Crate name:
+ librawssg_fs
Description: A filesystem
+ abstraction layer for static site generators. Defines the
+ FileSystem trait with a comprehensive set of file and directory
+ operations, and provides a concrete implementation RealFs that
+ delegates to std::fs and walkdir. The trait
+ includes built‑in path traversal protection and convenience methods for
+ atomic operations.
+
FileSystem
+ read_to_string
+ read_bytes
+ write
+ create_dir_all
+ remove_dir_all
+ remove_file
+ create_dir
+ exists
+ is_dir
+ is_file
+ read_dir
+ copy_file
+ copy_dir_all
+ walk_dir
+ canonicalize
+ rename
+ atomic_write
+ touch
+ metadata
+ symlink_metadata
+ permissions
+ set_permissions
+ read_link
+ hard_link
+ is_symlink
+ canonicalize_or_join
+ safe_join
+ copy
+ rename_or_copy
+ RealFs
+
+ FileSystem
+
+ librawssg_fs provides a trait‑based abstraction over filesystem
+ operations. This allows static site generator components to interact with the
+ filesystem without being tightly coupled to std::fs. It enables:
+
safe_join and canonicalize_or_join.
+ The crate exports:
+pub trait FileSystem – The main abstraction.pub struct RealFs – A zero‑sized type that implements
+ FileSystem using the real OS filesystem.
+
+ The crate root (lib.rs) defines the
+ FileSystem trait and re‑exports RealFs from the
+ real module.
+
pub mod real;
+pub use real::RealFs;
+
+
+ There is also an internal module real.rs containing the
+ RealFs implementation.
+
FileSystem
+ The FileSystem trait is the core of this crate. It is
+ object‑safe and requires implementors to be Send + Sync (safe to
+ share across threads). The trait provides many required methods and several
+ methods with default implementations.
+
pub trait FileSystem: Send + Sync {
+ // Required methods (see below)
+ // Provided methods with default implementations
+}
+
+
+ These methods must be implemented by any type that
+ implements FileSystem. They map closely to
+ std::fs functions and walkdir functionality.
+
read_to_stringfn read_to_string(&self, path: &Path) -> io::Result<String>;
+
+
+ Purpose: Reads the entire contents of a file into a
+ String.
+
Parameters:
+path: The path to the file to read.
+ Returns: Ok(String) containing the file
+ contents, or an Err(io::Error) if the file cannot be read (e.g.,
+ not found, permission denied, invalid UTF‑8).
+
Example:
+let content = fs.read_to_string(Path::new("hello.txt"))?;
+
+read_bytesfn read_bytes(&self, path: &Path) -> io::Result<Vec<u8>>;
+
++ Purpose: Reads the entire contents of a file as raw bytes. +
+Parameters:
+path: The path to the file.
+ Returns: Ok(Vec<u8>) with the file bytes,
+ or an Err(io::Error).
+
Example:
+let data = fs.read_bytes(Path::new("image.png"))?;
+
+writefn write(&self, path: &Path, content: &[u8]) -> io::Result<()>;
+
++ Purpose: Writes the given bytes to a file, creating any + necessary parent directories. +
+Parameters:
+path: Destination file path.content: Bytes to write.
+ Returns: Ok(()) on success, or
+ Err(io::Error) on failure (e.g., permission denied, disk full).
+
+ Behavior: The default RealFs implementation
+ creates parent directories before writing (via create_dir_all on
+ the parent). This is convenient for writing deeply nested outputs.
+
Example:
+fs.write(Path::new("a/b/c.txt"), b"hello")?;
+
+create_dir_allfn create_dir_all(&self, path: &Path) -> io::Result<()>;
+
++ Purpose: Creates a directory and all its missing parents. +
+Parameters:
+path: The directory path to create.
+ Returns: Ok(()) or Err(io::Error).
+
+ Note: Unlike create_dir, this does
+ not error if the directory already exists.
+
Example:
+fs.create_dir_all(Path::new("a/b/c"))?;
+
+remove_dir_allfn remove_dir_all(&self, path: &Path) -> io::Result<()>;
+
++ Purpose: Removes a directory and all its contents + recursively. +
+Parameters:
+path: Directory path to remove.
+ Returns: Ok(()) or
+ Err(io::Error) (e.g., directory does not exist, permission
+ denied).
+
Warning: This is destructive and cannot be undone.
+remove_filefn remove_file(&self, path: &Path) -> io::Result<()>;
+
+Purpose: Deletes a single file.
+Parameters:
+path: File path to remove.
+ Returns: Ok(()) or Err(io::Error).
+
create_dirfn create_dir(&self, path: &Path) -> io::Result<()>;
+
++ Purpose: Creates a single directory. Fails if the parent + directory does not exist or if the directory already exists. +
+Parameters:
+path: Directory path to create.
+ Returns: Ok(()) or
+ Err(io::Error) (e.g., already exists, parent missing).
+
existsfn exists(&self, path: &Path) -> bool;
+
++ Purpose: Checks whether a path exists (as a file, directory, + symlink, etc.). +
+Parameters:
+path: Path to check.
+ Returns: true if the path exists,
+ false otherwise.
+
+ Note: This method does not follow symlinks for broken
+ symlinks; it returns false for a broken symlink.
+
is_dirfn is_dir(&self, path: &Path) -> bool;
+
+Purpose: Checks whether the path points to a directory.
+
+ Returns: true if it is a directory,
+ false otherwise (including if it does not exist).
+
is_filefn is_file(&self, path: &Path) -> bool;
+
++ Purpose: Checks whether the path points to a regular file. +
+
+ Returns: true if it is a regular file,
+ false otherwise.
+
read_dirfn read_dir(&self, path: &Path) -> io::Result<Vec<PathBuf>>;
+
++ Purpose: Lists all entries (files and directories) directly + inside a directory. +
+Parameters:
+path: Directory path.
+ Returns: Ok(Vec<PathBuf>) containing the
+ full paths of all entries, or Err(io::Error).
+
+ Note: The order is not guaranteed. It does not recurse into + subdirectories. +
+copy_filefn copy_file(&self, from: &Path, to: &Path) -> io::Result<u64>;
+
+
+ Purpose: Copies a file from from to
+ to. If to already exists, it will be overwritten.
+
Parameters:
+from: Source file path.to: Destination file path.
+ Returns: Ok(u64) with the number of bytes
+ copied, or Err(io::Error).
+
+ Note: Does not create parent directories of
+ to in the default RealFs; use copy or
+ copy_dir_all for that.
+
copy_dir_allfn copy_dir_all(&self, from: &Path, to: &Path) -> io::Result<()>;
+
+
+ Purpose: Recursively copies a directory tree from
+ from to to. Creates the destination directory and
+ all parent directories as needed.
+
Parameters:
+from: Source directory path.to: Destination directory path.
+ Returns: Ok(()) or Err(io::Error).
+
Behavior:
+to directory.from (using walk_dir).to, then copies the file.
+ walk_dirfn walk_dir(&self, root: &Path) -> io::Result<Vec<PathBuf>>;
+
+
+ Purpose: Recursively collects all
+ files under root. Does not include directories
+ or symlinks to directories.
+
Parameters:
+root: Root directory to traverse.
+ Returns: Ok(Vec<PathBuf>) with the full
+ paths of all files, or Err(io::Error).
+
+ Note: The default RealFs uses the
+ walkdir crate to handle traversal. It follows symlinks? (The
+ WalkDir::new default does not follow symlinks; it will include
+ symlinks but not traverse into them unless
+ .follow_links(true) is set. Here symlinks to files will be
+ included? entry.file_type().is_file() will be true for a symlink
+ to a file? Actually file_type() returns the type of the symlink
+ itself, not the target, unless follow_links is used. So symlinks
+ are not considered files and are skipped.)
+
canonicalizefn canonicalize(&self, path: &Path) -> io::Result<PathBuf>;
+
+
+ Purpose: Returns the canonical, absolute form of a path,
+ resolving all symbolic links and normalizing . and
+ .. components.
+
Parameters:
+path: The path to canonicalize.
+ Returns: Ok(PathBuf) with the canonical path,
+ or Err(io::Error) (e.g., path does not exist).
+
renamefn rename(&self, from: &Path, to: &Path) -> io::Result<()>;
+
+
+ Purpose: Renames (moves) a file or directory from
+ from to to. On most filesystems this is an atomic
+ operation when source and destination are on the same filesystem.
+
Parameters:
+from: Source path.to: Destination path.
+ Returns: Ok(()) or Err(io::Error).
+
+ Note: If to exists, it may be overwritten
+ (platform‑dependent). Does not work across different mount points (returns
+ CrossesDevices error).
+
atomic_writefn atomic_write(&self, path: &Path, content: &[u8]) -> io::Result<()>;
+
++ Purpose: Writes data to a file atomically by first writing + to a temporary file and then renaming it over the target path. +
+Parameters:
+path: Destination file path.content: Bytes to write.
+ Returns: Ok(()) or Err(io::Error).
+
Behavior:
+.tmp (by calling
+ with_extension("tmp") on the target path).
+ write, which
+ creates parent directories).
+ rename).
+ + Note: The temporary file name is derived from the target; it + is not a hidden file and may collide if multiple writes happen concurrently + to the same path. This is a best‑effort atomic write suitable for many use + cases. +
+touchfn touch(&self, path: &Path) -> io::Result<()>;
+
+
+ Purpose: Creates an empty file at path or
+ updates its access/modification timestamp if it already exists.
+
Parameters:
+path: File path.
+ Returns: Ok(()) or Err(io::Error).
+
Behavior:
+write).sync_all() to flush to disk (optional, but ensures
+ metadata is updated).
+ Note: Existing file content is preserved.
+metadatafn metadata(&self, path: &Path) -> io::Result<std::fs::Metadata>;
+
++ Purpose: Returns metadata for a file or directory, following + symlinks. +
+Parameters:
+path: Path to query.
+ Returns: Ok(fs::Metadata) or
+ Err(io::Error).
+
symlink_metadatafn symlink_metadata(&self, path: &Path) -> io::Result<std::fs::Metadata>;
+
++ Purpose: Returns metadata for a path + without following symlinks (i.e., metadata of the symlink + itself). +
+Parameters:
+path: Path to query.
+ Returns: Ok(fs::Metadata) or
+ Err(io::Error).
+
permissionsfn permissions(&self, path: &Path) -> io::Result<std::fs::Permissions>;
+
+Purpose: Reads the permissions of a file or directory.
+Parameters:
+path: Path to query.
+ Returns: Ok(fs::Permissions) or
+ Err(io::Error).
+
+ Note: The default RealFs obtains permissions
+ from metadata, which follows symlinks.
+
set_permissionsfn set_permissions(&self, path: &Path, permissions: std::fs::Permissions) -> io::Result<()>;
+
+Purpose: Sets the permissions of a file or directory.
+Parameters:
+path: Target path.permissions: New permissions.
+ Returns: Ok(()) or Err(io::Error).
+
read_linkfn read_link(&self, path: &Path) -> io::Result<PathBuf>;
+
+Purpose: Reads the target of a symbolic link.
+Parameters:
+path: Path to the symlink.
+ Returns: Ok(PathBuf) containing the link
+ target, or Err(io::Error) if the path is not a symlink or does
+ not exist.
+
hard_linkfn hard_link(&self, from: &Path, to: &Path) -> io::Result<()>;
+
+
+ Purpose: Creates a hard link from from to
+ to.
+
Parameters:
+from: Existing file path.to: New hard link path.
+ Returns: Ok(()) or Err(io::Error).
+
Note: Both paths must be on the same filesystem.
++ These methods have default implementations that rely on the required methods. + Implementors may override them for performance or platform‑specific behavior. +
+is_symlinkfn is_symlink(&self, path: &Path) -> bool {
+ self.symlink_metadata(path)
+ .is_ok_and(|meta| meta.file_type().is_symlink())
+}
+
++ Purpose: Checks whether the given path is a symbolic link. +
+
+ Returns: true if the path is a symlink (even if
+ broken), false otherwise.
+
+ Implementation: Uses symlink_metadata (which
+ does not follow symlinks) and checks the file type.
+
Example:
+if fs.is_symlink(Path::new("link")) { ... }
+
+canonicalize_or_joinfn canonicalize_or_join(&self, base: &Path, candidate: &Path) -> io::Result<PathBuf>
+
+
+ Purpose: Safely resolves a possibly non‑existent path
+ relative to base. If the joined path exists, it is
+ canonicalized; otherwise it returns the canonical parent joined with the file
+ name, after normalizing . and .. components.
+
Parameters:
+base: The base directory (usually already canonical).candidate: A relative path (may contain . and
+ ..).
+
+ Returns: Ok(PathBuf) with the resolved path, or
+ Err(io::Error) if path traversal is detected or other errors
+ occur.
+
Detailed Behavior:
+candidate path by iterating over its
+ components:
+ CurDir (.) is ignored.ParentDir (..) causes the last normal
+ component to be popped. If there is no previous normal component
+ (i.e., attempt to go above root), it returns
+ PermissionDenied with message
+ "path traversal detected".
+ Prefix and RootDir components are pushed
+ (though they are unusual for relative candidates and may cause
+ issues later).
+ Normal components are pushed.base.
+ Security: This method prevents .. from escaping
+ the base directory (unless there are symlinks that point outside;
+ canonicalization of existing paths can still lead outside base, which is why
+ safe_join adds an extra check). For non‑existent paths, the
+ parent canonicalization ensures that the final path is within the canonical
+ base.
+
Example (from tests):
+let existing = base.join("existing.txt");
+fs.write(&existing, b"data")?;
+
+let canon_existing = fs.canonicalize_or_join(base, Path::new("existing.txt"))?;
+let canon_direct = fs.canonicalize(&existing)?;
+assert_eq!(canon_existing, canon_direct);
+
+let missing = Path::new("missing.txt");
+let canon_missing = fs.canonicalize_or_join(base, missing)?;
+let canon_base = fs.canonicalize(base)?;
+assert_eq!(canon_missing, canon_base.join(missing));
+
+safe_joinfn safe_join(&self, base: &Path, candidate: &Path) -> io::Result<PathBuf>
+
++ Purpose: Safely joins a candidate path to a base directory, + ensuring the result is within the base directory (no path + traversal). This is the recommended way to compute destination paths for + user‑provided or untrusted relative paths. +
+Parameters:
+base: The base directory (can be relative; it will be
+ canonicalized internally).
+ candidate: A relative path (may contain . and
+ ..).
+
+ Returns: Ok(PathBuf) with the resolved path,
+ guaranteed to start with the canonical base. Returns
+ Err(io::Error) with PermissionDenied if the
+ resolved path escapes the base (e.g.,
+ candidate = "../secret").
+
Implementation Details:
+base.canonicalize_or_join with the canonical base and
+ candidate.
+ PermissionDenied.
+
+ Why needed: Although
+ canonicalize_or_join prevents simple .. traversal,
+ symlinks inside the base directory could cause a resolved path to point
+ outside the base even after normalization. The starts_with check
+ enforces containment.
+
Example:
+let base = tmp.path().join("base");
+fs.create_dir_all(&base)?;
+
+let safe = fs.safe_join(&base, Path::new("inside.txt"))?;
+assert!(safe.starts_with(&base));
+
+let traversal = Path::new("../escape.txt");
+let result = fs.safe_join(&base, traversal);
+assert!(result.is_err());
+
+copyfn copy(&self, from: &Path, to: &Path) -> io::Result<()>
+
+
+ Purpose: Copies a file or directory from
+ from to to. If from is a directory, it
+ recursively copies the whole tree; if it is a file, it performs a single file
+ copy.
+
Parameters:
+from: Source path.to: Destination path.
+ Returns: Ok(()) or Err(io::Error).
+
Implementation:
+if self.is_dir(from) {
+ self.copy_dir_all(from, to)
+} else {
+ self.copy_file(from, to).map(|_| ())
+}
+
+
+ Note: Does not create parent directories of
+ to for file copies (unless copy_file implementation
+ does; the default RealFs::copy_file does not). For directories,
+ copy_dir_all does create to and parents as needed.
+
Example:
+fs.copy(&src_file, &dst_file)?;
+fs.copy(&src_dir, &dst_dir)?;
+
+rename_or_copyfn rename_or_copy(&self, from: &Path, to: &Path) -> io::Result<()>
+
+
+ Purpose: Attempts to rename from to
+ to. If the rename fails with
+ ErrorKind::CrossesDevices (i.e., source and destination are on
+ different filesystems), it falls back to copying the directory tree and then
+ removing the source.
+
Parameters:
+from: Source path.to: Destination path.
+ Returns: Ok(()) or Err(io::Error).
+
Behavior:
+rename(from, to).Ok(()).CrossesDevices:
+ copy_dir_all(from, to) to copy contents.remove_dir_all(from) to delete source.Ok(()).
+ Note: The fallback only works for directories (as the code
+ uses copy_dir_all and remove_dir_all). For a file
+ across devices, this will likely fail. This method is useful for moving
+ directories across mount points.
+
Example:
+fs.rename_or_copy(&src_dir, &dst_dir)?;
+
+RealFs
+ RealFs is a zero‑sized struct that implements
+ FileSystem by delegating directly to the operating system’s
+ filesystem APIs.
+
#[derive(Debug, Default, Clone, Copy)]
+pub struct RealFs;
+
+
+ It has no fields and can be instantiated with RealFs or
+ RealFs::default().
+
RealFs uses:
std::fs for most operations.walkdir::WalkDir for walk_dir.tracing::instrument attribute is applied to most methods
+ for logging (though tracing is not enabled by default; it can
+ be used with a subscriber).
+
+ All methods follow the behavior described in the trait definitions. The
+ write method creates parent directories before writing, and
+ atomic_write uses a temporary .tmp file.
+
use librawssg_fs::{FileSystem, RealFs};
+use std::path::Path;
+
+let fs = RealFs;
+
+// Write a file
+fs.write(Path::new("output/file.txt"), b"Hello")?;
+
+// Read it back
+let content = fs.read_to_string(Path::new("output/file.txt"))?;
+assert_eq!(content, "Hello");
+
+// Create directory
+fs.create_dir_all(Path::new("output/sub"))?;
+
+// Copy directory
+fs.copy_dir_all(Path::new("output"), Path::new("backup"))?;
+
+
+ All methods that can fail return io::Result<T> (i.e.,
+ Result<T, std::io::Error>). This is the standard error
+ type from the standard library, so no custom error enum is defined in this
+ crate. Consumers can inspect the error kind (e.g.,
+ ErrorKind::NotFound, PermissionDenied,
+ CrossesDevices) to handle specific failures.
+
+ The provided security methods (canonicalize_or_join and
+ safe_join) return io::Error with
+ ErrorKind::PermissionDenied when path traversal is detected,
+ along with the message "path traversal detected".
+
FileSystem
+
+ To create a mock filesystem or an alternative backend, implement the
+ FileSystem trait. You must provide implementations for all 24
+ required methods. The provided methods can be left as default unless you need
+ custom behavior.
+
Example of a minimal mock (from tests, adapted):
+use librawssg_fs::FileSystem;
+use std::io;
+use std::path::{Path, PathBuf};
+
+struct DummyFs;
+
+impl FileSystem for DummyFs {
+ fn read_to_string(&self, _path: &Path) -> io::Result<String> {
+ Err(io::Error::other("not implemented"))
+ }
+ // ... implement all other required methods similarly
+ // (returning Err or trivial values)
+}
+
+
+ Because FileSystem is Send + Sync, your mock must
+ also be thread‑safe. In practice, you can use Arc or interior
+ mutability if state is needed.
+
+ The library includes two methods specifically designed to prevent path + traversal attacks: +
+canonicalize_or_join: Normalizes .. and . and ensures the final
+ path does not go above the base (in terms of lexical components). However,
+ it may still follow symlinks that point outside the base if the path
+ exists.
+ safe_join: Combines canonicalize_or_join with a
+ starts_with check on the canonical base, providing a stronger
+ guarantee that the result is contained within the base directory.
+
+ Recommendation: Always use safe_join when
+ constructing output paths from untrusted input (e.g., user‑supplied relative
+ URLs). Avoid using join directly followed by canonicalization
+ without containment checks.
+
+ The test file tests/filesystem.rs contains comprehensive tests
+ for RealFs and the provided methods. It uses
+ tempfile::TempDir to create isolated temporary directories. The
+ tests cover:
+
copy_file, copy_dir_all, rename,
+ remove_file, remove_dir_all
+ atomic_write (including nested paths and overwriting)touch (creating new and preserving existing content)walk_dir (recursive collection)canonicalize_or_join (existing and missing paths)safe_join (rejecting traversal, allowing dot segments inside)
+ All tests can be run with cargo test.
+ Below are selected examples from the test suite that illustrate common usage + patterns. They can be copied and adapted. +
+use librawssg_fs::{FileSystem, RealFs};
+use std::path::Path;
+use tempfile::TempDir;
+
+let tmp = TempDir::new().unwrap();
+let fs = RealFs;
+let file_path = tmp.path().join("hello.txt");
+
+fs.write(&file_path, b"world").unwrap();
+let content = fs.read_to_string(&file_path).unwrap();
+assert_eq!(content, "world");
+
+let file = tmp.path().join("atomic.txt");
+fs.atomic_write(&file, b"first").unwrap();
+fs.atomic_write(&file, b"second").unwrap();
+let content = fs.read_to_string(&file).unwrap();
+assert_eq!(content, "second");
+
+let base = tmp.path().join("base");
+fs.create_dir_all(&base).unwrap();
+
+let safe = fs.safe_join(&base, Path::new("inside.txt")).unwrap();
+assert!(safe.starts_with(&base));
+
+let traversal = Path::new("../escape.txt");
+assert!(fs.safe_join(&base, traversal).is_err());
+
+let src = tmp.path().join("src_dir");
+let dst = tmp.path().join("dst_dir");
+fs.create_dir_all(&src.join("nested")).unwrap();
+fs.write(&src.join("file1.txt"), b"one").unwrap();
+fs.write(&src.join("nested").join("file2.txt"), b"two").unwrap();
+
+fs.copy_dir_all(&src, &dst).unwrap();
+assert!(fs.exists(&dst.join("file1.txt")));
+assert!(fs.exists(&dst.join("nested").join("file2.txt")));
+
+walk_dir to Gather All Files
+let root = tmp.path().join("root");
+fs.create_dir_all(&root.join("sub")).unwrap();
+fs.write(&root.join("root.txt"), b"root").unwrap();
+fs.write(&root.join("sub").join("sub.txt"), b"sub").unwrap();
+
+let files = fs.walk_dir(&root).unwrap();
+assert_eq!(files.len(), 2);
+
+
+ librawssg_fs provides a robust, thread‑safe filesystem
+ abstraction with built‑in path traversal protection and convenience methods
+ for atomic operations and cross‑device moves. The
+ RealFs implementation is ready to use, and the trait enables
+ easy mocking for unit tests. The extensive test suite validates all features
+ and serves as living documentation.
+
For any additional details, refer to the source code and inline comments.
diff --git a/docs/src/content/api/handler.raw b/docs/src/content/api/handler.raw new file mode 100644 index 0000000..552dc07 --- /dev/null +++ b/docs/src/content/api/handler.raw @@ -0,0 +1,1007 @@ +
+ Version: 1.0.0 (implied)
Crate name:
+ librawssg_handler
Description: Core data
+ structures and traits for building a static site generator (SSG) handler.
+ Provides Document, Metadata, and a
+ Processor trait for processing content files.
+
Document
+ new()
+ relative_url()
+ add_taxonomy()
+ depth()
+ with_list_items()
+ Metadata
+ new()
+ is_draft()
+ insert_extra()
+ get_extra()
+ Processor
+
+
+ librawssg_handler is the core library for a static site
+ generator. It defines the essential data structures used to represent a
+ processed document (Document) and its front matter
+ (Metadata). Additionally, it provides a pluggable
+ Processor trait that allows different file types to be processed
+ into Document instances.
+
The crate is intended to be used in conjunction with:
+librawssg_error: Provides the Error and
+ Result types for consistent error handling.
+ librawssg_fs: Defines a FileSystem trait
+ abstracting file I/O operations (used by Processor).
+ The library is organized into three public modules:
+document – Contains the
+ Document struct.
+ metadata – Contains the
+ Metadata struct.
+ processor – Contains the
+ Processor trait.
+ All public types are re‑exported at the crate root for convenience:
+pub use document::Document;
+pub use metadata::Metadata;
+pub use processor::Processor;
+
+Document+ Represents a fully processed content item ready for rendering or further + processing. +
+#[derive(Debug, Clone, PartialEq, Serialize)]
+#[non_exhaustive]
+pub struct Document {
+ pub metadata: Metadata,
+ pub body: String,
+ pub url: String,
+ pub output_path: PathBuf,
+ pub source_path: PathBuf,
+ pub depth: usize,
+ pub content_type: String,
+ pub is_list: bool,
+ pub list_items: Option<Vec<Self>>,
+ pub taxonomies: HashMap<String, Vec<String>>,
+}
+
+| Field | +Type | +Description | +
|---|---|---|
metadata |
+ Metadata |
+ Front matter metadata associated with the document. | +
body |
+ String |
+ + The processed content body (e.g., rendered HTML, Markdown text, + etc.). + | +
url |
+ String |
+
+ The relative URL where the document will be accessible (e.g.,
+ "blog/my-post.html").
+ |
+
output_path |
+ PathBuf |
+
+ Filesystem path where the final output file should be written (e.g.,
+ "blog/my-post/index.html").
+ |
+
source_path |
+ PathBuf |
+
+ Path to the original source file (e.g.,
+ "content/blog/my-post.md").
+ |
+
depth |
+ usize |
+ + Depth of the document in the site hierarchy (0 for top‑level). Used + for sorting or navigation. + | +
content_type |
+ String |
+
+ Identifier for the kind of content (e.g.,
+ "blog", "page",
+ "article").
+ |
+
is_list |
+ bool |
+ + Indicates whether this document represents a list of other documents + (e.g., an index page). + | +
list_items |
+ Option<Vec<Document>> |
+
+ If is_list is true, may contain the child documents.
+ None otherwise or when not set.
+ |
+
taxonomies |
+ HashMap<String, Vec<String>> |
+
+ A map of taxonomy names (e.g., "categories",
+ "tags") to lists of terms.
+ |
+
+++ Note: The
+#[non_exhaustive]attribute means + that external crates cannot exhaustively match onDocumentor + construct it with a struct literal; they must use the provided constructor + or update syntax. This allows adding fields in the future without breaking + downstream code. +
pub fn new(
+ metadata: Metadata,
+ body: impl Into<String>,
+ url: impl Into<String>,
+ output_path: impl Into<PathBuf>,
+ source_path: impl Into<PathBuf>,
+ depth: usize,
+ content_type: impl Into<String>,
+ is_list: bool,
+) -> Result<Self>
+
+
+ Purpose: Creates a new Document after
+ validating the provided arguments.
+
Parameters:
+metadata: A fully constructed Metadata instance.
+ body: The content body (accepts any type convertible to
+ String).
+ url: The desired relative URL (must not be empty or
+ whitespace only).
+ output_path: The target output path (must not be empty).
+ source_path: The source file path (must have a file name
+ component).
+ depth: The hierarchy depth (must be ≤ 1000).content_type: A string identifying the content type (e.g.,
+ "blog", "page").
+ is_list: Boolean indicating whether this document is a list
+ container.
+ Returns:
+Ok(Document) on success.Err(librawssg_error::Error::Validation(message)) if any
+ validation rule fails.
+ Validation Rules:
+url must not be empty or contain only whitespace.output_path must not be empty (as an OS string).source_path must have a file name (i.e., its last component
+ is not .. or empty).
+ depth must not exceed 1000.Example:
+use librawssg_handler::{Document, Metadata};
+use std::path::PathBuf;
+
+let metadata = Metadata::new("My Post", "A short description")?;
+
+let document = Document::new(
+ metadata,
+ "<h1>Hello</h1><p>World</p>",
+ "blog/my-post.html",
+ "blog/my-post/index.html",
+ "content/blog/my-post.md",
+ 1,
+ "blog",
+ false,
+)?;
+
+#[must_use]
+pub fn relative_url(&self) -> &str
+
+Purpose: Returns the relative URL of the document.
+
+ Returns: A string slice referencing the
+ url field.
+
Example:
+let doc = /* ... */;
+assert_eq!(doc.relative_url(), "blog/my-post.html");
+
+pub fn add_taxonomy(&mut self, name: impl Into<String>, items: Vec<String>)
+
+
+ Purpose: Inserts or replaces a taxonomy entry in the
+ document’s taxonomies map.
+
Parameters:
+name: Taxonomy name (converted into String).
+ items: A vector of string terms belonging to that taxonomy.
+ + Behavior: If a taxonomy with the same name already exists, + its value is replaced. +
+Example:
+let mut doc = /* ... */;
+doc.add_taxonomy("categories", vec!["rust".to_string(), "ssg".to_string()]);
+assert_eq!(doc.taxonomies["categories"], vec!["rust", "ssg"]);
+
+#[must_use]
+pub const fn depth(&self) -> usize
+
+Purpose: Returns the depth field.
Returns: The document’s depth as a usize.
Example:
+let doc = /* ... */;
+assert_eq!(doc.depth(), 1);
+
+#[must_use]
+pub fn with_list_items(mut self, items: Vec<Self>) -> Self
+
+
+ Purpose: Consumes the document, sets its
+ list_items field to Some(items), and returns the
+ modified document.
+
Parameters:
+items: A vector of Document instances that are
+ children of this list document.
+
+ Returns: The same document with list_items set.
+
Example:
+let parent = Document::new(/* ... */)?;
+let child1 = Document::new(/* ... */)?;
+let child2 = Document::new(/* ... */)?;
+let list_doc = parent.with_list_items(vec![child1, child2]);
+assert!(list_doc.list_items.is_some());
+
+Metadata+ Represents front matter (metadata) for a document. The struct is serializable + and deserializable, making it suitable for parsing from formats like YAML or + TOML front matter. +
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
+#[non_exhaustive]
+pub struct Metadata {
+ pub title: String,
+ pub description: String,
+ pub author: Option<String>,
+ pub repo_url: Option<String>,
+ pub license: Option<String>,
+ pub date: Option<NaiveDate>,
+ pub updated: Option<NaiveDate>,
+ pub tags: Vec<String>,
+ pub draft: bool,
+ pub extra: HashMap<String, serde_json::Value>,
+}
+
+| Field | +Type | +Description | +
|---|---|---|
title |
+ String |
+ The document title (required, cannot be empty). | +
description |
+ String |
+ A short description of the content. | +
author |
+ Option<String> |
+ The author’s name, if known. | +
repo_url |
+ Option<String> |
+ URL to the source repository. | +
license |
+ Option<String> |
+
+ License identifier (e.g., "MIT",
+ "Apache-2.0").
+ |
+
date |
+ Option<NaiveDate> |
+
+ Publication date (ISO 8601 date, e.g., 2026-09-08).
+ |
+
updated |
+ Option<NaiveDate> |
+ Last modification date. | +
tags |
+ Vec<String> |
+ List of tags (keywords) associated with the content. | +
draft |
+ bool |
+ + If true, the document is considered a draft and may be excluded from + builds. + | +
extra |
+ HashMap<String, serde_json::Value> |
+ Arbitrary extra key–value pairs for custom metadata. | +
+++ Note:
+#[non_exhaustive]prevents exhaustive + struct literals outside the crate; use the provided constructors or update + syntax. +
pub fn new(
+ title: impl Into<String>,
+ description: impl Into<String>,
+) -> librawssg_error::Result<Self>
+
+
+ Purpose: Creates a Metadata instance with a
+ required title and description. All other fields
+ are set to their default values.
+
Parameters:
+title: The title (must not be empty or whitespace only).
+ description: A description string.Returns:
+Ok(Metadata) on success.Err(librawssg_error::Error::Validation("metadata title cannot be
+ empty"))
+ if the title is empty or whitespace.
+ Example:
+use librawssg_handler::Metadata;
+
+let meta = Metadata::new("My Title", "My Description")?;
+assert_eq!(meta.title, "My Title");
+assert!(!meta.draft);
+assert!(meta.tags.is_empty());
+
+#[must_use]
+pub const fn is_draft(&self) -> bool
+
+Purpose: Returns the draft field.
+ Returns: true if the document is marked as a
+ draft, otherwise false.
+
Example:
+let mut meta = Metadata::new("Title", "Desc")?;
+assert!(!meta.is_draft());
+meta.draft = true;
+assert!(meta.is_draft());
+
+pub fn insert_extra(&mut self, key: impl Into<String>, value: impl Into<serde_json::Value>)
+
+
+ Purpose: Inserts or updates an entry in the
+ extra map.
+
Parameters:
+key: The key (converted to String).value: Any type convertible to
+ serde_json::Value (e.g., strings, numbers, booleans, arrays,
+ objects, or serde_json::json! macro results).
+ + Behavior: If the key already exists, its value is + overwritten. +
+Example:
+use serde_json::json;
+
+let mut meta = Metadata::new("Title", "Desc")?;
+meta.insert_extra("key", "value");
+meta.insert_extra("number", 42);
+meta.insert_extra("flag", true);
+meta.insert_extra("nested", json!({"foo": "bar"}));
+
+#[must_use]
+pub fn get_extra(&self, key: &str) -> Option<&serde_json::Value>
+
+
+ Purpose: Retrieves a reference to the value stored under
+ key in the extra map.
+
Parameters:
+key: The key to look up.Returns:
+Some(&Value) if the key exists.None otherwise.Example:
+let meta = /* ... */;
+if let Some(v) = meta.get_extra("key") {
+ assert_eq!(v, &json!("value"));
+}
+
+
+ Metadata derives both Serialize and
+ Deserialize, so it can be converted to/from JSON, YAML, etc.
+ This is particularly useful for reading front matter from source files.
+
Serialization Example:
+use serde_json;
+
+let meta = Metadata::new("Hello", "World")?;
+let json_str = serde_json::to_string(&meta)?;
+// {"title":"Hello","description":"World","author":null,...}
+
+Deserialization Example:
+let json_str = r#"{
+ "title": "Hello",
+ "description": "World",
+ "author": "Alice",
+ "date": "2026-09-08",
+ "tags": ["rust", "ssg"],
+ "draft": false,
+ "extra": {"foo": "bar"}
+}"#;
+
+let meta: Metadata = serde_json::from_str(json_str)?;
+assert_eq!(meta.author.as_deref(), Some("Alice"));
+
+
+ The Default trait is implemented. All fields are set to sensible
+ empty values:
+
title: empty stringdescription: empty stringauthor, repo_url, license,
+ date, updated: None
+ tags: empty vectordraft: falseextra: empty HashMapExample:
+let meta = Metadata::default();
+assert_eq!(meta.title, "");
+assert!(!meta.draft);
+assert!(meta.tags.is_empty());
+
+Processor
+ The Processor trait defines an interface for components that can
+ transform a source file into a Document. Multiple processors may
+ be registered and invoked based on their ability to handle a given file.
+
pub trait Processor: Send + Sync {
+ fn name(&self) -> &str;
+
+ fn priority(&self) -> i32 {
+ 0
+ }
+
+ fn can_process(&self, relative_path: &Path, original_path: &Path) -> bool;
+
+ fn process(
+ &self,
+ fs: &dyn FileSystem,
+ relative_path: &Path,
+ content_dir: &Path,
+ ) -> Result<Option<Document>>;
+}
+
+name()fn name(&self) -> &str
+
+
+ Purpose: Returns a human‑readable identifier for the
+ processor (e.g., "markdown",
+ "sass").
+
can_process()fn can_process(&self, relative_path: &Path, original_path: &Path) -> bool
+
++ Purpose: Determines whether this processor should handle the + given file. +
+Parameters:
+relative_path: The path of the file relative to the content
+ directory.
+ original_path: The full original path (often the same as
+ content_dir.join(relative_path)).
+
+ Returns: true if the processor can process this
+ file; false otherwise.
+
+ Typical Implementation: Check file extension or other + attributes. +
+Example:
+fn can_process(&self, relative_path: &Path, _original_path: &Path) -> bool {
+ relative_path.extension().and_then(|e| e.to_str()) == Some("md")
+}
+
+process()fn process(
+ &self,
+ fs: &dyn FileSystem,
+ relative_path: &Path,
+ content_dir: &Path,
+) -> Result<Option<Document>>
+
+
+ Purpose: Reads the source file, processes it, and returns an
+ optional Document.
+
Parameters:
+fs: A reference to a FileSystem implementation
+ for performing I/O operations.
+ relative_path: Path of the source file relative to the
+ content directory.
+ content_dir: The root directory containing all source
+ content.
+ Returns:
+Ok(Some(document)) if processing succeeded and produced a
+ document.
+ Ok(None) if the processor decides not to produce a document
+ (e.g., the file is ignored).
+ Err(librawssg_error::Error) if an error occurred during
+ processing.
+
+ Note: The FileSystem trait is defined in the
+ librawssg_fs crate. It abstracts many common file operations,
+ allowing processors to be tested with mock filesystems.
+
priority()fn priority(&self) -> i32 {
+ 0
+}
+
+
+ Purpose: Returns the priority of this processor. Processors
+ with higher priority are invoked before those with lower priority. The
+ default is 0.
+
+ Usage: Allows ordering of processors when multiple might + handle the same file. +
+Example:
+fn priority(&self) -> i32 {
+ 10
+}
+
+
+ To create a custom processor, implement the Processor trait.
+ Below is a complete example based on the test suite:
+
use librawssg_error::Result;
+use librawssg_fs::FileSystem;
+use librawssg_handler::{Document, Metadata, Processor};
+use std::path::Path;
+
+struct MarkdownProcessor;
+
+impl Processor for MarkdownProcessor {
+ fn name(&self) -> &str {
+ "markdown"
+ }
+
+ fn can_process(&self, relative_path: &Path, _original_path: &Path) -> bool {
+ relative_path.extension().and_then(|e| e.to_str()) == Some("md")
+ }
+
+ fn process(
+ &self,
+ fs: &dyn FileSystem,
+ relative_path: &Path,
+ content_dir: &Path,
+ ) -> Result<Option<Document>> {
+ // Read the source file
+ let source_path = content_dir.join(relative_path);
+ let content = fs.read_to_string(&source_path)?;
+
+ // Parse front matter and body (simplified here)
+ let metadata = Metadata::new("Untitled", "")?;
+ let body = content; // In reality, you would render Markdown to HTML
+
+ // Construct Document
+ let doc = Document::new(
+ metadata,
+ body,
+ relative_path.with_extension("html").to_string_lossy().to_string(),
+ relative_path.with_extension("index.html").to_string_lossy().into(),
+ source_path,
+ 1,
+ "page",
+ false,
+ )?;
+
+ Ok(Some(doc))
+ }
+}
+
+
+ The library uses the librawssg_error::Error enum for all
+ fallible operations. Relevant variants:
+
Error::Validation(String) – Used when a validation rule fails
+ (e.g., empty URL, invalid depth).
+ Error::Processor(String) – Used by processors to signal
+ processing errors.
+
+ The return type Result<T> is an alias for
+ std::result::Result<T, librawssg_error::Error>.
+
Example of Validation Error:
+let result = Document::new(meta, "body", "", "out", "src.md", 0, "page", false);
+assert!(matches!(
+ result,
+ Err(librawssg_error::Error::Validation(ref msg)) if msg == "document url cannot be empty"
+));
+
+FileSystem Trait
+ The Processor::process method takes a
+ &dyn FileSystem. This trait is defined in
+ librawssg_fs and provides a comprehensive set of file operations
+ (read, write, create directories, walk, etc.). A typical implementation wraps
+ std::fs, but for testing, mock implementations are often used.
+
+ A minimal FileSystem implementation (used in tests) might
+ implement all methods returning
+ io::Error::other("not implemented") for those not
+ needed.
+
+ The test suite contains numerous examples that demonstrate correct usage and + error conditions. Below are selected examples. +
+use librawssg_handler::{Document, Metadata};
+
+let metadata = Metadata::new("Title", "Description")?;
+let doc = Document::new(
+ metadata,
+ "<p>Body</p>",
+ "blog/my-post.html",
+ "blog/my-post/index.html",
+ "content/blog/my-post.md",
+ 1,
+ "blog",
+ false,
+)?;
+
+assert_eq!(doc.metadata.title, "Title");
+assert_eq!(doc.body, "<p>Body</p>");
+
+let result = Document::new(
+ Metadata::new("Title", "Desc")?,
+ "body",
+ "", // empty URL
+ "out",
+ "src.md",
+ 0,
+ "page",
+ false,
+);
+assert!(result.is_err());
+
+let mut doc = /* ... */;
+doc.add_taxonomy("categories", vec!["rust".to_string(), "ssg".to_string()]);
+
+extra Metadata
+use serde_json::json;
+
+let mut meta = Metadata::new("Title", "Desc")?;
+meta.insert_extra("key", "value");
+if let Some(v) = meta.get_extra("key") {
+ assert_eq!(v, &json!("value"));
+}
+
+struct MockProcessor { /* ... */ }
+
+impl Processor for MockProcessor {
+ fn name(&self) -> &str { "mock" }
+ fn can_process(&self, _: &Path, _: &Path) -> bool { true }
+ fn process(&self, _fs: &dyn FileSystem, _rel: &Path, _cd: &Path) -> Result<Option<Document>> {
+ Ok(Some(document))
+ }
+}
+
+Metadata::newtitle must not be empty or contain only whitespace.Document::newurl must not be empty or contain only whitespace.output_path must not be empty (as an OS string).source_path must have a file name component.depth must be ≤ 1000.Any violation results in an Err(Error::Validation(...)).
The tests are organized into four files:
+document_tests.rs – Validates
+ Document construction, field defaults, error cases, and
+ methods (relative_url, add_taxonomy,
+ depth, with_list_items).
+ metadata_tests.rs – Tests
+ Metadata creation, validation, is_draft,
+ insert_extra/get_extra, default values, and JSON
+ serialization/deserialization round‑trip.
+ processor_tests.rs – Tests the
+ Processor trait using mock implementations:
+ name, priority, can_process,
+ process returning Some, None, and
+ error.
+ unit_tests.rs – Verifies that all public
+ items are re‑exported at the crate root.
+ All tests can serve as executable examples of the API usage.
+
+ This documentation covers the public API of librawssg_handler in
+ detail. The crate provides a flexible foundation for building static site
+ generators by separating metadata handling (Metadata), document
+ representation (Document), and pluggable processing logic
+ (Processor). The validation rules ensure data integrity, and the
+ use of traits like FileSystem enables testability.
+
+ For further details, refer to the source code and the accompanying test + suite. +
diff --git a/docs/src/content/api/index.raw b/docs/src/content/api/index.raw new file mode 100644 index 0000000..460bf45 --- /dev/null +++ b/docs/src/content/api/index.raw @@ -0,0 +1,14 @@ +Welcome to the API documentation for the librawssg workspace. Each crate is documented individually, covering its public types, traits, functions, and usage examples.
Select a crate from the list above to view its complete API documentation, including methods, fields, examples, and testing notes.
diff --git a/docs/src/content/api/templates.raw b/docs/src/content/api/templates.raw new file mode 100644 index 0000000..1c67df2 --- /dev/null +++ b/docs/src/content/api/templates.raw @@ -0,0 +1,947 @@ +
+ Version: 1.0.0 (implied)
+ Crate name: librawssg_templates
+ Description: Defines rendering abstractions for static site
+ generators. Provides the Renderer and
+ RenderContext traits, and an optional
+ TeraRenderer implementation (when the tera feature
+ is enabled) that integrates the Tera template engine.
+
RenderContext
+ Renderer
+ TeraRenderer
+ new()
+ add_raw_template()
+ add_template_file()
+ add_template_files_from_dir()
+ load_templates_dir()
+ enable_autoescape()
+ render_str()
+ as_tera()
+ as_tera_mut()
+ Default
+ Renderer for
+ TeraRenderer
+ RenderContext for
+ tera::Context
+
+ librawssg_templates provides a pluggable template rendering
+ system. It abstracts the rendering process with two traits:
+
Renderer – Defines the
+ render method that takes a template name and a context,
+ returning a rendered string.
+ RenderContext – An object‑safe trait that
+ allows type erasure for context objects; specifically, it provides
+ as_any and as_mut_any to downcast to concrete
+ context types.
+
+ The crate optionally includes a
+ TeraRenderer implementation for the
+ Tera template engine. This
+ implementation is gated behind the tera feature flag.
+
The crate root (lib.rs) declares:
pub mod renderer;
+#[cfg(feature = "tera")]
+pub mod tera_renderer;
+
+pub use renderer::{RenderContext, Renderer};
+#[cfg(feature = "tera")]
+pub use tera_renderer::TeraRenderer;
+
+renderer – Always available; contains the
+ two core traits.
+ tera_renderer – Only compiled when the
+ tera feature is enabled; contains
+ TeraRenderer.
+ TeraRenderer easy to import.
+
+ The tera feature must be explicitly enabled in
+ Cargo.toml to use TeraRenderer. Without it, the
+ crate still provides the traits for custom renderer implementations.
+
RenderContextpub trait RenderContext: Send + Sync {
+ fn as_any(&self) -> &dyn Any;
+ fn as_mut_any(&mut self) -> &mut dyn Any;
+}
+
+
+ Purpose: Allows arbitrary context types to be passed to a
+ Renderer as a trait object. The renderer can then downcast the
+ &dyn RenderContext to the concrete context type it expects
+ (e.g., tera::Context). This provides flexibility without
+ requiring all renderers to accept a single concrete type.
+
Requirements:
+Send + Sync (thread‑safe).as_any and as_mut_any to expose
+ the underlying Any reference.
+ Typical Implementation:
+For any type T, you can implement:
impl RenderContext for T {
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+ fn as_mut_any(&mut self) -> &mut dyn Any {
+ self
+ }
+}
+
+Example (from tests):
+struct MockContext;
+
+impl RenderContext for MockContext {
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+ fn as_mut_any(&mut self) -> &mut dyn Any {
+ self
+ }
+}
+
+
+Rendererpub trait Renderer: Send + Sync {
+ fn render(&self, template_name: &str, context: &dyn RenderContext) -> Result<String>;
+}
+
+
+ Purpose: Defines the rendering interface. A
+ Renderer takes a template identifier (name) and a context
+ object, and returns the rendered output as a String.
+
Parameters:
+template_name: A string identifying the template (e.g.,
+ "index.html", "blog/post.tera").
+ context: A reference to an object implementing
+ RenderContext. The renderer is expected to downcast this to
+ the appropriate concrete context type.
+ Returns:
+Ok(String) containing the rendered output.Err(librawssg_error::Error) if rendering fails (e.g.,
+ template not found, invalid syntax, missing variable, or context type
+ mismatch).
+ Note: Implementors must be Send + Sync.
Example (custom mock renderer):
+struct MockRenderer { output: String }
+
+impl Renderer for MockRenderer {
+ fn render(&self, _template_name: &str, _context: &dyn RenderContext) -> Result<String> {
+ Ok(self.output.clone())
+ }
+}
+
+
+TeraRenderer
+ TeraRenderer is a wrapper around tera::Tera,
+ providing convenient methods to load templates and render them using the
+ Renderer trait. It is only available when the
+ tera feature is enabled.
+
#[derive(Debug)]
+pub struct TeraRenderer {
+ tera: tera::Tera,
+}
+
+
+ The tera field is private; access is provided via
+ as_tera and as_tera_mut.
+
TeraRenderer::new#[must_use]
+pub fn new() -> Self
+
+
+ Purpose: Creates a new TeraRenderer with an
+ empty Tera instance (tera::Tera::default()).
+
Returns: A new TeraRenderer.
Example:
+let renderer = TeraRenderer::new();
+
+
+TeraRenderer::add_raw_template
+pub fn add_raw_template(&mut self, name: &str, content: &str) -> Result<()>
+
+
+ Purpose: Adds a template from a string, associating it with
+ the given name. The template is parsed and stored internally.
+
Parameters:
+name: The template name (e.g., "index.html",
+ "partial").
+ content: The raw template source (e.g.,
+ "Hello {{ name }}").
+ Returns:
+Ok(()) if the template was added successfully.Err(Error::Render) if the template syntax is invalid (the
+ underlying tera::Error is converted to a string and
+ wrapped).
+
+ Behavior: Calls
+ tera.add_raw_template(name, content). Template names must be
+ unique; adding a duplicate name will replace the existing template.
+
Example:
+renderer.add_raw_template("hello", "Hello {{ name }}")?;
+
+
+TeraRenderer::add_template_file
+pub fn add_template_file(&mut self, path: &Path) -> Result<()>
+
++ Purpose: Reads a template file from disk and adds it to the + renderer. The template name is derived from the file name (including + extension). +
+Parameters:
+path: Path to the template file.Returns:
+Ok(()) on success.Err(Error::Io) if the file cannot be read (wrapped as
+ Error::Io with a message containing the original I/O
+ error).
+ Err(Error::Render) if the file name is not valid UTF‑8 or
+ missing (unlikely).
+ Behavior:
+std::fs::read_to_string. On
+ failure, maps to
+ Error::Io(std::io::Error::other(format!("{e}"))).
+ &str. If missing or non‑UTF‑8, returns
+ Error::Render("template file has no valid file name").
+ add_raw_template with that file name as the template
+ name.
+ Example:
+renderer.add_template_file(Path::new("templates/index.html"))?;
+// Template is registered as "index.html"
+
+
+TeraRenderer::add_template_files_from_dir
+pub fn add_template_files_from_dir(&mut self, dir: &Path) -> Result<()>
+
++ Purpose: Adds all files directly inside a directory as + templates. This method is not recursive; it only considers + files in the immediate directory. +
+Parameters:
+dir: Directory containing template files.Returns:
+Ok(()) if at least the directory is readable and processing
+ completes.
+ Err(Error::Io) on directory read failure.Err(Error::Render) if any individual file cannot be added.
+ Behavior:
+std::fs::read_dir.add_template_file with its path.
+ + Note: Template names are the file names (including + extensions). +
+Example:
+renderer.add_template_files_from_dir(Path::new("templates/"))?;
+// Adds all files in templates/ as templates with names like "base.tera", "index.html", etc.
+
+
+TeraRenderer::load_templates_dir
+pub fn load_templates_dir(&mut self, dir: &Path) -> Result<()>
+
+
+ Purpose: Recursively loads all template files from a
+ directory tree. Template names are derived from the relative path (using
+ forward slashes as separators), allowing nested template structures (e.g.,
+ "sub/nested.tera").
+
Parameters:
+dir: Root directory to traverse.Returns:
+Ok(()) on success.Err(Error::Io) for filesystem errors during traversal or
+ reading.
+ Err(Error::Render) for invalid UTF‑8 paths or component
+ issues.
+ Behavior:
+dir.canonicalize()) to ensure a stable base.
+ walkdir::WalkDir.
+ Only files are processed.
+ strip_prefix.
+ rel_path_to_template_name (which joins
+ components with / and rejects non‑normal
+ components).
+ add_raw_template with the computed template
+ name and content.
+
+ Note: This method is similar to
+ add_template_files_from_dir but recursive and with
+ namespace‑like template names.
+
Example:
+renderer.load_templates_dir(Path::new("templates"))?;
+// If templates contains sub/child.tera, it can be referenced as "sub/child.tera"
+
+
+TeraRenderer::enable_autoescape
+pub fn enable_autoescape(&mut self)
+
+
+ Purpose: Turns on automatic escaping for HTML, HTM, and XML
+ file extensions. This is a convenience method that calls
+ tera.autoescape_on(vec!["html", "htm", "xml"]).
+
Parameters: None.
+Returns: Nothing.
+
+ Behavior: After calling this, templates with names ending
+ in .html, .htm, or .xml will
+ automatically escape variable output (HTML escaping). For other file
+ extensions, autoescaping remains off.
+
Example:
+renderer.enable_autoescape();
+renderer.add_raw_template("page.html", "{{ user_input }}")?;
+// Rendering will escape HTML special characters in user_input
+
+
+TeraRenderer::render_strpub fn render_str(&self, template_str: &str, context: &dyn RenderContext) -> Result<String>
+
++ Purpose: Renders a one‑off template string without + registering it. This is useful for small, inline templates. +
+Parameters:
+template_str: The template source as a string.context: A &dyn RenderContext that must
+ downcast to tera::Context.
+ Returns:
+Ok(String) with rendered output.Err(Error::Render) if the context is not a
+ tera::Context or if rendering fails.
+ Behavior:
+context.as_any() to
+ &tera::Context. If the cast fails, returns
+ Error::Render("invalid context type for Tera").
+ tera::Tera::one_off(template_str, tera_ctx, true).
+ The third argument true enables autoescaping for the
+ one‑off render by default, regardless of the renderer's
+ autoescape settings.
+
+ Note: render_str always autoescapes (the
+ true parameter forces autoescape). This may differ from
+ render which respects the renderer's autoescape configuration.
+
Example:
+let renderer = TeraRenderer::new();
+let mut ctx = tera::Context::new();
+ctx.insert("content", "<b>bold</b>");
+let output = renderer.render_str("{{ content }}", &ctx)?;
+// Output is escaped: "<b>bold</b>"
+
+
+TeraRenderer::as_tera#[must_use]
+pub const fn as_tera(&self) -> &tera::Tera
+
+
+ Purpose: Returns an immutable reference to the underlying
+ tera::Tera instance. This allows advanced operations not
+ directly exposed by TeraRenderer.
+
Returns: &tera::Tera.
Example:
+let tera = renderer.as_tera();
+// e.g., inspect registered templates
+
+
+TeraRenderer::as_tera_mut#[must_use]
+pub const fn as_tera_mut(&mut self) -> &mut tera::Tera
+
+
+ Purpose: Returns a mutable reference to the underlying
+ tera::Tera instance. Useful for direct manipulation, such as
+ adding templates or changing settings.
+
Returns: &mut tera::Tera.
Example:
+let tera_mut = renderer.as_tera_mut();
+tera_mut.add_raw_template("direct", "Hello")?;
+
+
+TeraRenderer::Defaultimpl Default for TeraRenderer {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+
+ Allows creating a TeraRenderer with
+ TeraRenderer::default(), equivalent to new().
+
Renderer for TeraRenderer
+impl Renderer for TeraRenderer {
+ fn render(&self, template_name: &str, context: &dyn RenderContext) -> Result<String> {
+ let tera_ctx = context
+ .as_any()
+ .downcast_ref::<tera::Context>()
+ .ok_or_else(|| Error::Render("invalid context type for Tera".into()))?;
+ self.tera
+ .render(template_name, tera_ctx)
+ .map_err(|e| Error::Render(e.to_string()))
+ }
+}
+
+render method:
+ tera::Context.tera.render(template_name, tera_ctx).
+ Error::Render.RenderContext for tera::Context
+impl RenderContext for tera::Context {
+ fn as_any(&self) -> &dyn core::any::Any {
+ self
+ }
+ fn as_mut_any(&mut self) -> &mut dyn core::any::Any {
+ self
+ }
+}
+
+
+ This implementation allows tera::Context to be used directly as
+ a RenderContext when calling render. Users
+ typically create a tera::Context, populate it, and pass
+ &ctx to render.
+
rel_path_to_template_name (private)
fn rel_path_to_template_name(rel_path: &Path) -> Result<String>
+
+
+ Purpose: Converts a relative path (from
+ strip_prefix) into a template name string using forward slashes
+ as separators. It rejects paths with unusual components (prefixes, root,
+ parent, or current directory).
+
Parameters:
+rel_path: A relative path (assumed to have been stripped of
+ a base).
+ Returns:
+Ok(String) with the template name (e.g.,
+ "sub/nested.tera").
+ Err(Error::Render) if:
+ Normal (e.g., contains
+ .. or / absolute parts).
+
+ Note: This function is not public but is essential for
+ load_templates_dir.
+
+ All fallible methods in TeraRenderer return
+ librawssg_error::Result<T>. The errors originate from:
+
Error::Io (with
+ std::io::Error::other wrapper to preserve the original
+ message).
+ Error::Render with the error message as string.
+ Error::Render("invalid context type for Tera").
+ Error::Render.
+
+ The Renderer trait method also returns
+ Result<String>, allowing custom renderers to use the same
+ error type.
+
Renderer, RenderContext) are
+ always available.
+ TeraRenderer and the tera integration are only
+ compiled when the tera feature is enabled.
+ TeraRenderer are also gated with
+ #![cfg(feature = "tera")].
+ To enable the feature, add to Cargo.toml:
[dependencies]
+librawssg_templates = { version = "...", features = ["tera"] }
+
+
+
+ The test suite (tera_tests.rs and unit_tests.rs)
+ provides extensive examples. Below are selected snippets with explanations.
+
let mut renderer = TeraRenderer::new();
+renderer.add_raw_template("simple", "{{ title }}")?;
+
+let mut ctx = tera::Context::new();
+ctx.insert("title", "Hello World");
+let output = renderer.render("simple", &ctx)?;
+assert_eq!(output, "Hello World");
+
+
+renderer.add_raw_template("loop", "{% for item in items %}{{ item }}{% if not loop.last %},{% endif %}{% endfor %}")?;
+// context: items = ["a","b","c"] -> output "a,b,c"
+
+renderer.add_raw_template("filter", "{{ title | upper }}")?;
+// context: title = "Hello" -> "HELLO"
+
+renderer.add_raw_template("condition", "{% if number > 40 %}high{% else %}low{% endif %}")?;
+// context: number = 42 -> "high"
+
+
+// Add a single file
+let file_path = dir.path().join("hello.tera");
+std::fs::write(&file_path, "{{ name }}")?;
+renderer.add_template_file(&file_path)?;
+// Template name is "hello.tera"
+
+// Add all files from a directory (non-recursive)
+renderer.add_template_files_from_dir(dir.path())?;
+
+// Recursive loading with namespaced names
+renderer.load_templates_dir(dir.path())?;
+// If sub/nested.tera exists, use renderer.render("sub/nested.tera", &ctx)
+
+
+renderer.enable_autoescape();
+renderer.add_raw_template("esc.html", "{{ content }}")?;
+let mut ctx = tera::Context::new();
+ctx.insert("content", "<script>alert(1)</script>");
+let output = renderer.render("esc.html", &ctx)?;
+// Output: <script>alert(1)</script>
+
+Note: render_str always autoescapes:
let output = renderer.render_str("{{ content }}", &ctx)?;
+// Also escaped
+
+
+renderer.add_raw_template("base", "<html>{% block content %}Default{% endblock %}</html>")?;
+renderer.add_raw_template("child", "{% extends \"base\" %}{% block content %}Child content{% endblock %}")?;
+// Rendering "child" yields "<html>Child content</html>"
+
+renderer.add_raw_template("macro", "{% macro hello(name) %}Hello, {{ name }}{% endmacro hello %}{{ self::hello(name=\"World\") }}")?;
+// Rendering "macro" yields "Hello, World"
+
+
+let ctx = sample_context();
+let dyn_ctx: &dyn RenderContext = &ctx;
+assert!(dyn_ctx.as_any().is::<tera::Context>());
+
+
+ This shows how the RenderContext trait enables type erasure and
+ safe downcasting.
+
The crate contains two test files:
+unit_tests.rs (always compiled): Tests the
+ core traits using mock implementations, verifies re‑exports are
+ available, and ensures the traits can be used without the
+ tera feature.
+ tera_tests.rs (compiled only with
+ tera feature): Comprehensive tests for
+ TeraRenderer including:
+ Tera via
+ as_tera and as_tera_mut.
+
+ The tests use tempfile for temporary directories and
+ walkdir for directory traversal validation.
+
+ librawssg_templates offers a flexible and extensible template
+ rendering abstraction. The core Renderer and
+ RenderContext traits allow any template engine to be
+ integrated, while the built‑in TeraRenderer provides a
+ powerful, full‑featured implementation for the Tera engine. With methods for
+ loading templates from files or directories, autoescaping control, and
+ direct access to the underlying engine, it covers the needs of most static
+ site generators.
+
+ The crate is designed with testability and thread‑safety in mind, and the
+ comprehensive test suite serves as both documentation and validation. By
+ enabling the tera feature, developers can immediately start
+ rendering templates with minimal setup.
+
+ We as members, contributors, and leaders pledge to make participation in our + community a harassment-free experience for everyone, regardless of age, body + size, visible or invisible disability, ethnicity, sex characteristics, gender + identity and expression, level of experience, education, socio-economic status, + nationality, personal appearance, race, religion, or sexual identity and + orientation. +
++ We pledge to act and interact in ways that contribute to an open, welcoming, + diverse, inclusive, and healthy community. +
+ +Examples of behavior that contributes to a positive environment for our community include:
+Examples of unacceptable behavior include:
++ Community leaders are responsible for clarifying and enforcing our standards of + acceptable behavior and will take appropriate and fair corrective action in + response to any behavior that they deem inappropriate, threatening, offensive, + or harmful. +
++ Community leaders have the right and responsibility to remove, edit, or reject + comments, commits, code, wiki edits, issues, and other contributions that are + not aligned to this Code of Conduct, and will communicate reasons for moderation + decisions when appropriate. +
+ ++ This Code of Conduct applies within all community spaces, and also applies when + an individual is officially representing the community in public spaces. + Examples of representing our community include using an official e-mail address, + posting via an official social media account, or acting as an appointed + representative at an online or offline event. +
+ ++ Instances of abusive, harassing, or otherwise unacceptable behavior may be + reported to the community leaders responsible for enforcement at + mroczect@proton.me. + All complaints will be reviewed and investigated promptly and fairly. +
++ All community leaders are obligated to respect the privacy and security of the + reporter of any incident. +
+ ++ Community leaders will follow these Community Impact Guidelines in determining + the consequences for any action they deem in violation of this Code of Conduct: +
+ +Community Impact: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
+Consequence: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
+ +Community Impact: A violation through a single incident or series of actions.
+Consequence: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
+ +Community Impact: A serious violation of community standards, including sustained inappropriate behavior.
+Consequence: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
+ +Community Impact: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
+Consequence: A permanent ban from any sort of public interaction within the community.
+ ++ This Code of Conduct is adapted from the + Contributor Covenant, + version 2.0, available at + https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. +
++ Community Impact Guidelines were inspired by + Mozilla's code of conduct enforcement ladder. +
++ For answers to common questions about this code of conduct, see the FAQ at + https://www.contributor-covenant.org/faq. + Translations are available at + https://www.contributor-covenant.org/translations. +
\ No newline at end of file diff --git a/docs/src/content/configuration.raw b/docs/src/content/configuration.raw new file mode 100644 index 0000000..00e326c --- /dev/null +++ b/docs/src/content/configuration.raw @@ -0,0 +1,402 @@ +
+ librawssg uses a single configuration file to control the entire
+ build process. This file can be written in YAML or JSON format and contains
+ site settings, directory locations, content processing rules, and custom
+ extra data.
+
+ The configuration can be stored as config.yaml,
+ config.yml, or config.json. By default, the
+ PipelineBuilder supports reading YAML via its
+ load_config() method. For JSON, you can manually call
+ Config::from_json_str().
+
# config.yaml
+site:
+ site_name: "My Documentation"
+ description: "A site built with librawssg"
+ language: "en"
+ base_url: "https://example.com"
+ author: "Your Name"
+ repo_url: "https://github.com/username/repo"
+ license: "MIT"
+
+build:
+ content_dir: "content"
+ output_dir: "dist"
+ templates_dir: "templates"
+ static_dir: "static"
+
+content_rules:
+ - name: "page"
+ pattern: "**/*.raw"
+ template: "base.tera"
+ list_enabled: false
+
+
+site
+ Contains site metadata and navigation structures. All fields except
+ site_name are optional and have sensible defaults.
+
| Field | +Type | +Default | +Description | +
|---|---|---|---|
site_name |
+ string |
+ "librawssg" |
+ The name of the website. Must not be empty or whitespace-only. | +
description |
+ string | null |
+ null |
+ A short description of the site. | +
language |
+ string | null |
+ "en" |
+ Site language code (e.g., "en", "id"). |
+
base_url |
+ string | null |
+ null |
+ The base URL of the site. Must start with http:// or https:// if set. |
+
author |
+ string | null |
+ null |
+ Default author name. | +
repo_url |
+ string | null |
+ null |
+ URL to the source repository. | +
license |
+ string | null |
+ null |
+ License identifier (e.g., "MIT"). |
+
navbar |
+ array<NavItem> |
+ [] |
+ List of navigation items for the top bar. | +
sidebar |
+ array<NavItem> |
+ [] |
+ List of navigation items for the sidebar. | +
extra |
+ object |
+ {} |
+ Arbitrary extra site-wide metadata. | +
+ Each NavItem has the following fields:
+
{
+ "label": "Home",
+ "url": "/",
+ "children": []
+}
+
+label – Display text for the link.url – URL the link points to (relative or absolute).children – Optional nested sub-items for hierarchical menus.build+ Specifies the directory locations used during the build. +
+ +| Field | +Type | +Default | +Description | +
|---|---|---|---|
content_dir |
+ string |
+ "content" |
+ Directory containing source content files. | +
output_dir |
+ string |
+ "dist" |
+ Directory where generated site output will be written. | +
templates_dir |
+ string |
+ "templates" |
+ Directory containing template files. | +
static_dir |
+ string |
+ "static" |
+ Directory containing static assets (copied as-is). | +
content_rules+ A list of rules that map file patterns to templates. Each rule has the + following fields: +
+ +| Field | +Type | +Default | +Description | +
|---|---|---|---|
name |
+ string |
+ required | +A unique identifier for the rule (e.g., "blog", "page"). |
+
pattern |
+ string |
+ required | +Glob pattern matching content files (e.g., "**/*.md"). Must not contain ... |
+
template |
+ string |
+ required | +Name of the template to use for rendering each matched file. | +
list_template |
+ string | null |
+ null |
+ Optional template name for rendering list pages (index pages). | +
list_enabled |
+ boolean |
+ false |
+ Whether list generation is enabled for this rule. | +
extra |
+ object |
+ {} |
+ Arbitrary extra data associated with the rule. | +
content_rules:
+ - name: "blog"
+ pattern: "blog/**/*.md"
+ template: "post.tera"
+ list_template: "blog_list.tera"
+ list_enabled: true
+
+
+extra+ A free-form object for storing custom top-level data. This data can be + accessed from templates via the context. +
+ ++ The configuration is validated when the pipeline is built. The following + checks are performed: +
+ +site.site_name must not be empty or contain only whitespace.name, pattern, and template.pattern must not contain ".." (to prevent path traversal).site.base_url is set, it must begin with http:// or https://.
+ If any validation fails, an error of type
+ Error::Validation is returned with a descriptive message.
+
use librawssg_compiler::PipelineBuilder;
+
+let pipeline = PipelineBuilder::new()
+ .load_config("config.yaml")?
+ // ... other builder methods
+ .build()?;
+
+
+use librawssg_config::Config;
+
+let json_str = r#"{
+ "site": {
+ "site_name": "My Site"
+ },
+ "build": {},
+ "content_rules": [
+ {
+ "name": "page",
+ "pattern": "**/*.html",
+ "template": "base.tera"
+ }
+ ]
+}"#;
+
+let config = Config::from_json_str(json_str)?;
+
+use librawssg_config::{Config, ContentRule};
+
+let mut config = Config::new()
+ .with_site_name("My Site");
+
+config.add_content_rule(
+ ContentRule::new("page", "**/*.raw", "base.tera")
+);
+
+
+ The Config struct supports serialization to YAML and JSON:
+
let yaml = config.to_yaml_string()?;
+let json = config.to_json_string()?;
+
++ This allows saving the configuration back to a file for later use. +
+ +content_rules:
+ - name: "page"
+ pattern: "**/*.raw"
+ template: "base.tera"
+ - name: "blog"
+ pattern: "blog/**/*.md"
+ template: "post.tera"
+ list_enabled: true
+ list_template: "blog_list.tera"
+
+
+site:
+ navbar:
+ - label: "Home"
+ url: "/"
+ - label: "API"
+ url: "/api/index.html"
+ children:
+ - label: "Compiler"
+ url: "/api/compiler.html"
+ - label: "Config"
+ url: "/api/config.html"
+ sidebar:
+ - label: "Getting Started"
+ url: "/"
+ - label: "Installation"
+ url: "/installation.html"
+
+
+site:
+ extra:
+ analytics_id: "UA-123456-7"
+ social:
+ twitter: "handle"
+
+
+
+ This extra data is accessible in templates via {{ site.extra }}.
+
content_rules ordered from most specific to least
+ specific, as later rules take precedence.
+ build section can be omitted entirely; defaults will be
+ used.
+ list_enabled: true and provide a
+ list_template if you want index pages for a content type.
+ + Thank you for your interest in contributing to librawssg. This document + outlines the process for reporting issues, proposing changes, and submitting + code contributions. Following these guidelines helps maintain the quality and + consistency of the project. +
+ ++ This project adheres to a minimal set of social rules: be respectful, + constructive, and inclusive. Harassment, discrimination, or hostile behaviour + is not tolerated. If you experience or witness such conduct, please contact + the maintainers. +
+ +git clone https://github.com/YOUR_USERNAME/librawssg.git
+cd librawssg
+ git remote add upstream https://github.com/mroczect/librawssg.git
+ git checkout -b feat/my-feature
+ tera,
+ pulldown, serve) pull additional crates only when
+ enabled.
+ All commands below are run from the repository root.
+ +cargo build
+To build with all features enabled:
+cargo build --all-features
+
+cargo test
+cargo test --features tera,pulldown
+cargo test --all-features
+
+ This runs unit tests, integration tests (located in tests/), and
+ doc-tests. All tests must pass before a pull request is accepted.
+
cargo fmt --all -- --check
+cargo clippy --all-targets --all-features -- -D warnings
+These are enforced in CI. Run them locally to avoid surprises.
+ +cargo fmt).rustc and clippy lints strictly; any warning is treated as an error in CI.Result and Option appropriately.From implementations for error conversions./// comments.#[cfg(...)] attributes.+ Use + conventional commit format: +
+type(scope): short description
+
+Optional longer explanation.
+
+ Types: feat, fix, docs,
+ test, ci, chore, refactor,
+ style.
+
+ Scope: librawssg (for core library), ci,
+ docs, etc.
+
Examples:
+feat(librawssg): add support for custom content handlersfix(librawssg): prevent path traversal when outputting filesdocs(librawssg): add API reference for PageContextThis format enables automatic changelog generation and clear history.
+ +master.cargo test, cargo fmt --all -- --check, and cargo clippy --all-targets --all-features -- -D warnings to verify there are no issues.master branch of the main repository.Open an issue on GitHub and include:
+rustc --version), librawssg version or commit hash.Feature requests are welcome. When opening an issue:
+For large features, consider opening an issue first to gather feedback before writing code.
+ +cargo doc).Thank you for contributing to librawssg. Your effort helps make the project better for everyone.
\ No newline at end of file diff --git a/docs/src/content/index.raw b/docs/src/content/index.raw new file mode 100644 index 0000000..e17075f --- /dev/null +++ b/docs/src/content/index.raw @@ -0,0 +1,99 @@ ++ A modular static site generator library for Rust. Build your own static site + generator with composable, testable, and safe components. +
+ +
+ librawssg is a collection of Rust crates that provide the core
+ building blocks for creating a static site generator. It is not a
+ ready‑to‑use CLI tool, but a framework that gives you full control over your
+ site’s behaviour.
+
Processor trait.
+ Add the facade crate to your Cargo.toml:
[dependencies]
+librawssg = "1.0.0"
+
+
+ Then, in your main.rs, assemble a pipeline:
+
use librawssg::{Config, ContentRule, PipelineBuilder, RealFs, TeraContextBuilder, TeraRenderer};
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let mut config = Config::new().with_site_name("My Site");
+ config.add_content_rule(ContentRule::new("page", "**/*.raw", "base.tera"));
+
+ let mut renderer = TeraRenderer::new();
+ renderer.load_templates_dir("templates")?;
+
+ let pipeline = PipelineBuilder::new()
+ .config(config)
+ .content_dir("content")
+ .output_dir("dist")
+ .with_fs(Box::new(RealFs))
+ .with_renderer(Box::new(renderer))
+ .with_context_builder(Box::new(TeraContextBuilder))
+ .build()?;
+
+ pipeline.run()?;
+ Ok(())
+}
+
++ For a complete example, see the Installation + page. +
+ ++ The source code is available on + GitHub. +
diff --git a/docs/src/content/installation.raw b/docs/src/content/installation.raw new file mode 100644 index 0000000..977ee7d --- /dev/null +++ b/docs/src/content/installation.raw @@ -0,0 +1,191 @@ +
+ This guide covers how to install and set up librawssg for building
+ your own static site generator, or for integrating it into an existing Rust
+ project.
+
+ librawssg is a workspace of several crates. The easiest way is to
+ use the facade crate librawssg that re‑exports the essential
+ components.
+
Add the following to your Cargo.toml:
[dependencies]
+librawssg = "1.0.0"
+
+
+ If you are working within the same workspace as the librawssg
+ source, use a path dependency instead:
+
[dependencies]
+librawssg = { path = "../librawssg" }
+
+
+ The facade crate re‑exports librawssg_templates with the
+ tera feature enabled by default. This gives you
+ TeraRenderer and TeraContextBuilder. If you want to
+ disable the Tera integration (for a custom renderer), set
+ default-features = false:
+
[dependencies]
+librawssg = { version = "1.0.0", default-features = false }
+
+
+ To create a minimal static site generator using librawssg, follow
+ these steps:
+
Create a new binary crate:
+cargo new my-site-generator
+cd my-site-generator
+ Add dependencies to Cargo.toml:
[dependencies]
+librawssg = "1.0.0"
+ Create the necessary directories and files:
+mkdir -p content templates static
+ Place your content, templates, and static assets in the respective folders.
+Write a main.rs that builds and runs the pipeline (see example below).
+ The following example uses a simple raw HTML processor and the Tera renderer.
+ It processes .raw files, renders them with a base template, and
+ copies static files.
+
use librawssg::{
+ Config, ContentRule, Document, FileSystem, Metadata, PipelineBuilder, Processor,
+ RealFs, TeraContextBuilder, TeraRenderer,
+};
+use std::path::{Path, PathBuf};
+
+struct RawProcessor;
+
+impl Processor for RawProcessor {
+ fn name(&self) -> &'static str {
+ "raw"
+ }
+
+ fn can_process(&self, rel: &Path, _orig: &Path) -> bool {
+ rel.extension().and_then(|e| e.to_str()) == Some("raw")
+ }
+
+ fn process(
+ &self,
+ fs: &dyn FileSystem,
+ rel: &Path,
+ content_dir: &Path,
+ ) -> librawssg::Result<Option<Document>> {
+ let full_path = content_dir.join(rel);
+ let body = fs.read_to_string(&full_path)?;
+ let title = rel.file_stem().unwrap_or_default().to_string_lossy().to_string();
+ let meta = Metadata::new(title, String::new())?;
+ let url = rel.with_extension("html").to_string_lossy().to_string();
+ let output = PathBuf::from(&url);
+ let doc = Document::new(meta, body, url, output, rel.to_path_buf(), 0, "page".to_string(), false)?;
+ Ok(Some(doc))
+ }
+}
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ // 1. Configure the site
+ let mut config = Config::new().with_site_name("My Site");
+ config.add_content_rule(ContentRule::new("page", "**/*.raw", "base.tera"));
+ config.build.content_dir = "content".into();
+ config.build.output_dir = "dist".into();
+ config.build.static_dir = "static".into();
+
+ // 2. Set up the renderer and load templates
+ let mut renderer = TeraRenderer::new();
+ renderer.load_templates_dir(Path::new("templates"))?;
+
+ // 3. Build the pipeline
+ let pipeline = PipelineBuilder::new()
+ .config(config)
+ .content_dir("content")
+ .output_dir("dist")
+ .with_fs(Box::new(RealFs))
+ .with_renderer(Box::new(renderer))
+ .with_context_builder(Box::new(TeraContextBuilder))
+ .add_processor(Box::new(RawProcessor))
+ .build()?;
+
+ // 4. Run the generation
+ pipeline.run()?;
+ println!("Site generated in 'dist'");
+ Ok(())
+}
+
+
+ The librawssg repository includes a docs crate that
+ generates this documentation website. To build it locally:
+
cargo run -p docs
+
+
+ The output will be written to docs/dist/. You can open
+ index.html to view the site.
+
+ A typical project using librawssg has the following layout:
+
my-site-generator/
+├── Cargo.toml
+├── content/ # Source content files (.raw, .md, etc.)
+├── templates/ # Tera templates
+├── static/ # Static assets (CSS, JS, images)
+└── src/
+ └── main.rs # Entry point
+
+The MIT License (MIT)
+ +Copyright (c) 2026 mroczect
+ ++ Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: +
+ ++ The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. +
+ ++ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +
\ No newline at end of file diff --git a/docs/src/main.rs b/docs/src/main.rs new file mode 100644 index 0000000..e94213d --- /dev/null +++ b/docs/src/main.rs @@ -0,0 +1,126 @@ +#![allow(clippy::multiple_crate_versions)] + +use librawssg::{ + Config, ContentRule, Document, FileSystem, Metadata, NavItem, PipelineBuilder, Processor, + RealFs, TeraContextBuilder, TeraRenderer, +}; +use std::path::{Path, PathBuf}; + +struct RawFileProcessor; + +impl Processor for RawFileProcessor { + fn name(&self) -> &'static str { + "raw-file" + } + + fn can_process(&self, relative_path: &Path, _original_path: &Path) -> bool { + relative_path.extension().is_some_and(|ext| ext == "raw") + } + + fn process( + &self, + fs: &dyn FileSystem, + relative_path: &Path, + content_dir: &Path, + ) -> librawssg::Result