From b7b262718d6a8795fb0ff55a821f03c250716b58 Mon Sep 17 00:00:00 2001 From: trivernis Date: Wed, 28 Jun 2023 13:04:50 +0200 Subject: [PATCH] Add rendering implementation --- .gitignore | 1 + src/config.rs | 24 +++++++++++++ src/data/dir_loader.rs | 36 +++++++++++--------- src/data/index.rs | 12 +++---- src/data/mod.rs | 2 ++ src/data/page.rs | 10 ++++-- src/data/page_loader.rs | 34 +++++++++++++++++++ src/main.rs | 52 +++++++++++++++++++++++++++-- src/rendering/mod.rs | 74 +++++++++++++++++++++++++++++++++++++++++ 9 files changed, 217 insertions(+), 28 deletions(-) create mode 100644 src/config.rs create mode 100644 src/data/page_loader.rs create mode 100644 src/rendering/mod.rs diff --git a/.gitignore b/.gitignore index ea8c4bf..8fc646e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +test diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..fa5319f --- /dev/null +++ b/src/config.rs @@ -0,0 +1,24 @@ +use std::path::{Path, PathBuf}; + +use miette::{IntoDiagnostic, Result}; +use serde::Deserialize; +use tokio::fs; + +#[derive(Clone, Debug, Deserialize)] +pub struct Config { + pub folders: Folders, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct Folders { + pub content: Option, + pub templates: Option, + pub output: Option, +} + +pub async fn read_config(dir: &Path) -> Result { + let cfg_string = fs::read_to_string(dir.join("viki.toml")) + .await + .into_diagnostic()?; + toml::from_str(&cfg_string).into_diagnostic() +} diff --git a/src/data/dir_loader.rs b/src/data/dir_loader.rs index c1e73a5..4e546b6 100644 --- a/src/data/dir_loader.rs +++ b/src/data/dir_loader.rs @@ -15,6 +15,7 @@ pub struct DirLoader { #[derive(Clone, Debug)] pub struct FolderData { + pub content_root: PathBuf, pub path: PathBuf, pub index: IndexData, pub pages: Vec, @@ -44,23 +45,19 @@ impl DirLoader { } } - let results = futures::future::join_all(paths.into_iter().map(Self::read_dir)).await; - let mut folder_data = Vec::new(); - - for res in results { - match res { - Ok(Some(data)) => folder_data.push(data), - Err(e) => return Err(e), - _ => {} - } - } + let folder_data = + futures::future::try_join_all(paths.into_iter().map(|p| self.read_dir(p))) + .await? + .into_iter() + .filter_map(|f| f) + .collect(); Ok(folder_data) } - #[tracing::instrument(level = "trace")] - async fn read_dir(path: PathBuf) -> Result> { - let index_path = path.join("_index.md"); + #[tracing::instrument(level = "trace", skip(self))] + async fn read_dir(&self, path: PathBuf) -> Result> { + let index_path = path.join("_index.toml"); if !index_path.exists() { return Ok(None); @@ -72,6 +69,7 @@ impl DirLoader { path, index: index_data, pages, + content_root: self.base_path.to_owned(), })) } } @@ -101,6 +99,11 @@ async fn find_pages(dir: &Path, index_data: &IndexData) -> Result> let entry_path = entry.path(); if entry_path.is_file() + && !entry_path + .file_name() + .unwrap() + .to_string_lossy() + .starts_with("_") && include_set.is_match(&entry_path) && !excluded_set.is_match(&entry_path) { @@ -112,9 +115,12 @@ async fn find_pages(dir: &Path, index_data: &IndexData) -> Result> } #[tracing::instrument(level = "trace")] -fn build_glob_set(globs: &Vec) -> GlobSetBuilder { +fn build_glob_set(globs: &Vec) -> GlobSetBuilder { let mut builder = GlobSetBuilder::new(); - globs.iter().fold(&mut builder, |b, g| b.add(g.clone())); + globs + .iter() + .filter_map(|pattern| Glob::new(pattern).ok()) + .fold(&mut builder, |b, g| b.add(g)); builder } diff --git a/src/data/index.rs b/src/data/index.rs index 4ef7c42..9b4ec58 100644 --- a/src/data/index.rs +++ b/src/data/index.rs @@ -1,6 +1,3 @@ -use std::collections::HashMap; - -use globset::Glob; use serde::Deserialize; #[derive(Clone, Debug, Deserialize)] @@ -9,11 +6,10 @@ pub struct IndexData { pub default_template: Option, /// files that are included for rendering - pub include_files: Vec, + #[serde(default)] + pub include_files: Vec, /// files that are explicitly excluded from rendering - pub excluded_files: Vec, - - /// File paths with templates used to rendering them - pub templates: HashMap, + #[serde(default)] + pub excluded_files: Vec, } diff --git a/src/data/mod.rs b/src/data/mod.rs index 97919f5..e8aee24 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -1,7 +1,9 @@ mod dir_loader; mod index; mod page; +mod page_loader; pub use dir_loader::*; pub use index::*; pub use page::*; +pub use page_loader::*; diff --git a/src/data/page.rs b/src/data/page.rs index 76b0dde..7606c51 100644 --- a/src/data/page.rs +++ b/src/data/page.rs @@ -1,6 +1,12 @@ -use serde::Deserialize; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum Page { + Data(PageMetadata), + Content(String), +} + +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct PageMetadata { /// template used to render this page pub template: Option, diff --git a/src/data/page_loader.rs b/src/data/page_loader.rs new file mode 100644 index 0000000..19feae7 --- /dev/null +++ b/src/data/page_loader.rs @@ -0,0 +1,34 @@ +use std::path::Path; + +use miette::{Context, IntoDiagnostic, Result}; +use tokio::fs; + +use super::Page; + +pub struct PageLoader; + +/// loads a page and parses the data depending on the extension +#[tracing::instrument(level = "trace")] +pub async fn load_page(path: &Path) -> Result { + let string_content = load_string_content(path).await?; + + if let Some(extension) = path.extension() { + let extension_lower = extension.to_string_lossy().to_lowercase(); + match extension_lower.as_str() { + "toml" => Ok(Page::Data( + toml::from_str(&string_content).into_diagnostic()?, + )), + _ => Ok(Page::Content(string_content)), + } + } else { + Ok(Page::Content(string_content)) + } +} + +#[tracing::instrument(level = "trace")] +async fn load_string_content(path: &Path) -> Result { + fs::read_to_string(path) + .await + .into_diagnostic() + .context("reading page content") +} diff --git a/src/main.rs b/src/main.rs index 7722fd7..7e7ce2b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,53 @@ -pub mod args; +use args::BuildArgs; +use clap::Parser; +use config::{read_config, Config}; +use data::DirLoader; +use miette::Result; +use rendering::ContentRenderer; +use tracing::metadata::LevelFilter; +use tracing_subscriber::fmt::format::FmtSpan; + +use crate::args::Args; + +mod args; +mod config; pub mod data; +mod rendering; #[tokio::main] -async fn main() { - println!("Hello, world!"); +async fn main() -> Result<()> { + let args: Args = Args::parse(); + init_tracing(); + + match args.command { + args::Command::Build(build_args) => { + let cfg = read_config(&build_args.directory).await?; + build(cfg, build_args).await + } + } +} + +async fn build(cfg: Config, args: BuildArgs) -> Result<()> { + let folders = cfg.folders; + let base_path = args.directory; + let content_dir = base_path.join(folders.content.unwrap_or("content".into())); + let template_dir = base_path.join(folders.templates.unwrap_or("templates".into())); + let out_dir = base_path.join(folders.output.unwrap_or("public".into())); + + let dirs = DirLoader::new(content_dir).read_content().await?; + let template_glob = format!("{}/**/*", template_dir.to_string_lossy()); + ContentRenderer::new(template_glob, out_dir) + .render_all(dirs) + .await?; + + Ok(()) +} + +fn init_tracing() { + tracing_subscriber::fmt::SubscriberBuilder::default() + .with_max_level(LevelFilter::TRACE) + .with_writer(std::io::stderr) + .with_span_events(FmtSpan::NEW | FmtSpan::CLOSE) + .compact() + .init(); } diff --git a/src/rendering/mod.rs b/src/rendering/mod.rs new file mode 100644 index 0000000..1c75e20 --- /dev/null +++ b/src/rendering/mod.rs @@ -0,0 +1,74 @@ +use std::path::PathBuf; + +use futures::future; +use miette::{IntoDiagnostic, Result}; +use tera::{Context, Tera}; +use tokio::fs; + +use crate::data::{load_page, FolderData}; + +// renders content using the given template folder +pub struct ContentRenderer { + template_glob: String, + out_dir: PathBuf, +} + +impl ContentRenderer { + pub fn new(template_glob: String, out_dir: PathBuf) -> Self { + Self { + template_glob, + out_dir, + } + } + + #[tracing::instrument(level = "trace", skip_all)] + pub async fn render_all(&self, dirs: Vec) -> Result<()> { + if self.out_dir.exists() { + fs::remove_dir_all(&self.out_dir).await.into_diagnostic()?; + } + let tera = Tera::new(&self.template_glob).into_diagnostic()?; + future::try_join_all(dirs.into_iter().map(|data| self.render_folder(&tera, data))).await?; + + Ok(()) + } + + #[tracing::instrument(level = "trace", skip_all)] + async fn render_folder(&self, tera: &Tera, data: FolderData) -> Result<()> { + for page_path in data.pages { + let page = load_page(&page_path).await?; + let mut context = Context::new(); + let mut template_name = data + .index + .default_template + .to_owned() + .unwrap_or("default".into()); + + match page { + crate::data::Page::Data(data) => { + if let Some(tmpl) = data.template { + template_name = tmpl; + } + context.insert("data", &data.data); + } + crate::data::Page::Content(content) => context.insert("content", &content), + } + + tracing::debug!("context = {:?}", context); + + let html = tera.render(&template_name, &context).into_diagnostic()?; + let rel_path = page_path + .strip_prefix(&data.content_root) + .into_diagnostic()?; + let mut out_path = self.out_dir.join(rel_path); + out_path.set_extension("html"); + let parent = out_path.parent().unwrap(); + + if !parent.exists() { + fs::create_dir_all(parent).await.into_diagnostic()?; + } + fs::write(out_path, html).await.into_diagnostic()?; + } + + Ok(()) + } +}