Add rendering implementation

main
trivernis 11 months ago
parent 9cdcbfe7c6
commit b7b262718d
Signed by: Trivernis
GPG Key ID: DFFFCC2C7A02DB45

1
.gitignore vendored

@ -1 +1,2 @@
/target
test

@ -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<PathBuf>,
pub templates: Option<PathBuf>,
pub output: Option<PathBuf>,
}
pub async fn read_config(dir: &Path) -> Result<Config> {
let cfg_string = fs::read_to_string(dir.join("viki.toml"))
.await
.into_diagnostic()?;
toml::from_str(&cfg_string).into_diagnostic()
}

@ -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<PathBuf>,
@ -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<Option<FolderData>> {
let index_path = path.join("_index.md");
#[tracing::instrument(level = "trace", skip(self))]
async fn read_dir(&self, path: PathBuf) -> Result<Option<FolderData>> {
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<Vec<PathBuf>>
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<Vec<PathBuf>>
}
#[tracing::instrument(level = "trace")]
fn build_glob_set(globs: &Vec<Glob>) -> GlobSetBuilder {
fn build_glob_set(globs: &Vec<String>) -> 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
}

@ -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<String>,
/// files that are included for rendering
pub include_files: Vec<Glob>,
#[serde(default)]
pub include_files: Vec<String>,
/// files that are explicitly excluded from rendering
pub excluded_files: Vec<Glob>,
/// File paths with templates used to rendering them
pub templates: HashMap<Glob, String>,
#[serde(default)]
pub excluded_files: Vec<String>,
}

@ -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::*;

@ -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<String>,

@ -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<Page> {
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<String> {
fs::read_to_string(path)
.await
.into_diagnostic()
.context("reading page content")
}

@ -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();
}

@ -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<FolderData>) -> 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(())
}
}
Loading…
Cancel
Save