Compare commits
2 Commits
Author | SHA1 | Date |
---|---|---|
trivernis | ce20464337 | 1 year ago |
trivernis | 3c22c723ad | 1 year ago |
@ -0,0 +1,56 @@
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use async_walkdir::{Filtering, WalkDir};
|
||||
use futures::{future, StreamExt};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct TemplateName(String);
|
||||
|
||||
impl AsRef<str> for TemplateName {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Page {
|
||||
template: TemplateName,
|
||||
#[serde(flatten)]
|
||||
data: HashMap<String, toml::Value>,
|
||||
}
|
||||
|
||||
pub struct ContentLoader {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl ContentLoader {
|
||||
pub fn new(path: PathBuf) -> Self {
|
||||
Self { path }
|
||||
}
|
||||
|
||||
async fn load_pages(&self) -> Vec<Page> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn find_files(&self) -> Vec<PathBuf> {
|
||||
WalkDir::new(&self.path)
|
||||
.filter(|e| async move {
|
||||
e.path()
|
||||
.extension()
|
||||
.map(|e| {
|
||||
if e == "toml" {
|
||||
Filtering::Continue
|
||||
} else {
|
||||
Filtering::Ignore
|
||||
}
|
||||
})
|
||||
.unwrap_or(Filtering::Ignore)
|
||||
})
|
||||
.map(|e| e.expect("failed to read dir").path())
|
||||
.collect::<Vec<_>>()
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_page(path: PathBuf) {}
|
@ -1,8 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub struct Context {
|
||||
pub content_dir: PathBuf,
|
||||
pub template_dir: PathBuf,
|
||||
pub stylesheet_dir: PathBuf,
|
||||
pub output_dir: PathBuf,
|
||||
}
|
@ -1,124 +0,0 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use async_walkdir::WalkDir;
|
||||
use futures::StreamExt;
|
||||
use globset::{Glob, GlobSetBuilder};
|
||||
use miette::{Context, IntoDiagnostic, Result};
|
||||
use tokio::fs;
|
||||
|
||||
use super::IndexData;
|
||||
|
||||
/// loads directory data
|
||||
pub struct DirLoader {
|
||||
base_path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FolderData {
|
||||
pub path: PathBuf,
|
||||
pub index: IndexData,
|
||||
pub pages: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
impl DirLoader {
|
||||
pub fn new(base_path: PathBuf) -> Self {
|
||||
Self { base_path }
|
||||
}
|
||||
|
||||
/// Asynchronously reads all the entries at the given content location
|
||||
#[tracing::instrument(level = "trace", skip(self))]
|
||||
pub async fn read_content(&self) -> Result<Vec<FolderData>> {
|
||||
let mut entries = WalkDir::new(&self.base_path);
|
||||
let mut paths = Vec::new();
|
||||
paths.push(self.base_path.to_owned());
|
||||
|
||||
while let Some(res) = entries.next().await {
|
||||
match res {
|
||||
Ok(entry) => {
|
||||
let entry_path = entry.path();
|
||||
if entry_path.is_dir() {
|
||||
paths.push(entry_path)
|
||||
}
|
||||
}
|
||||
Err(e) => return Err(e).into_diagnostic(),
|
||||
}
|
||||
}
|
||||
|
||||
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", 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);
|
||||
}
|
||||
let index_data = read_index_data(&index_path).await?;
|
||||
let pages = find_pages(&path, &index_data).await?;
|
||||
|
||||
Ok(Some(FolderData {
|
||||
path,
|
||||
index: index_data,
|
||||
pages,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace")]
|
||||
async fn read_index_data(path: &Path) -> Result<IndexData> {
|
||||
let index_str = fs::read_to_string(path)
|
||||
.await
|
||||
.into_diagnostic()
|
||||
.context("reading index file")?;
|
||||
toml::from_str(&index_str).into_diagnostic()
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace")]
|
||||
async fn find_pages(dir: &Path, index_data: &IndexData) -> Result<Vec<PathBuf>> {
|
||||
let include_set = build_glob_set(&index_data.include_files)
|
||||
.build()
|
||||
.into_diagnostic()?;
|
||||
let excluded_set = build_glob_set(&index_data.excluded_files)
|
||||
.build()
|
||||
.into_diagnostic()?;
|
||||
|
||||
let mut read_dir = fs::read_dir(dir).await.into_diagnostic()?;
|
||||
let mut pages = Vec::new();
|
||||
|
||||
while let Some(entry) = read_dir.next_entry().await.into_diagnostic()? {
|
||||
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)
|
||||
{
|
||||
pages.push(entry_path);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(pages)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace")]
|
||||
fn build_glob_set(globs: &Vec<String>) -> GlobSetBuilder {
|
||||
let mut builder = GlobSetBuilder::new();
|
||||
globs
|
||||
.iter()
|
||||
.filter_map(|pattern| Glob::new(pattern).ok())
|
||||
.fold(&mut builder, |b, g| b.add(g));
|
||||
|
||||
builder
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct IndexData {
|
||||
/// the default template that is used for rendering
|
||||
pub default_template: Option<String>,
|
||||
|
||||
/// files that are included for rendering
|
||||
#[serde(default)]
|
||||
pub include_files: Vec<String>,
|
||||
|
||||
/// files that are explicitly excluded from rendering
|
||||
#[serde(default)]
|
||||
pub excluded_files: Vec<String>,
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
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,25 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub enum Page {
|
||||
Data(PageData),
|
||||
Content(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct PageData {
|
||||
/// Metadata for this page
|
||||
#[serde(default)]
|
||||
pub metadata: PageMetadata,
|
||||
|
||||
/// remaining data of this page
|
||||
/// passed to the templates when rendering
|
||||
#[serde(flatten)]
|
||||
pub data: toml::Value,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct PageMetadata {
|
||||
/// template used to render this page
|
||||
pub template: Option<String>,
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
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")
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use config::ConfigLoader;
|
||||
|
||||
use miette::Result;
|
||||
|
||||
mod config;
|
||||
mod content_loader;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Paths {
|
||||
pub config: PathBuf,
|
||||
}
|
||||
|
||||
pub struct Viki {
|
||||
config_loader: ConfigLoader,
|
||||
}
|
||||
|
||||
impl Viki {
|
||||
#[tracing::instrument(level = "trace")]
|
||||
pub async fn load(paths: Paths) -> Result<Self> {
|
||||
let config_loader = ConfigLoader::load(paths.config).await?;
|
||||
|
||||
Ok(Self { config_loader })
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
pub async fn reload(&mut self) -> Result<()> {
|
||||
self.config_loader.reload().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
use comrak::ComrakOptions;
|
||||
use tera::{try_get_value, Filter};
|
||||
|
||||
pub struct Markdown;
|
||||
|
||||
impl Filter for Markdown {
|
||||
fn filter(
|
||||
&self,
|
||||
value: &tera::Value,
|
||||
_args: &std::collections::HashMap<String, tera::Value>,
|
||||
) -> tera::Result<tera::Value> {
|
||||
let string_content = try_get_value!("markdown", "value", String, value);
|
||||
let html = comrak::markdown_to_html(&string_content, &ComrakOptions::default());
|
||||
|
||||
Ok(tera::Value::String(html))
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
use tera::Tera;
|
||||
mod markdown;
|
||||
|
||||
pub fn register_all(tera: &mut Tera) {
|
||||
tera_text_filters::register_all(tera);
|
||||
tera.register_filter("markdown", markdown::Markdown);
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
use tera::Tera;
|
||||
|
||||
mod filters;
|
||||
|
||||
pub fn register_all(tera: &mut Tera) {
|
||||
filters::register_all(tera);
|
||||
}
|
@ -1,126 +0,0 @@
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use futures::future;
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
use tera::{Context as TeraContext, Tera};
|
||||
use tokio::{fs, sync::Mutex};
|
||||
|
||||
use crate::{
|
||||
context::Context,
|
||||
data::{load_page, FolderData},
|
||||
};
|
||||
|
||||
use self::style::{load_stylesheets, Stylesheets};
|
||||
|
||||
mod style;
|
||||
|
||||
// renders content using the given template folder
|
||||
pub struct ContentRenderer {
|
||||
template_glob: String,
|
||||
ctx: Arc<Context>,
|
||||
styles: Arc<Mutex<Stylesheets>>,
|
||||
}
|
||||
|
||||
impl ContentRenderer {
|
||||
pub async fn new(ctx: Arc<Context>) -> Result<Self> {
|
||||
let template_glob = format!("{}/**/*", ctx.template_dir.to_string_lossy());
|
||||
let styles = load_stylesheets(&ctx.stylesheet_dir).await?;
|
||||
|
||||
Ok(Self {
|
||||
template_glob,
|
||||
ctx,
|
||||
styles: Arc::new(Mutex::new(styles)),
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
pub async fn render_all(&self, dirs: Vec<FolderData>) -> Result<()> {
|
||||
if self.ctx.output_dir.exists() {
|
||||
fs::remove_dir_all(&self.ctx.output_dir)
|
||||
.await
|
||||
.into_diagnostic()?;
|
||||
}
|
||||
let mut tera = Tera::new(&self.template_glob).into_diagnostic()?;
|
||||
super::processors::register_all(&mut tera);
|
||||
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<()> {
|
||||
let dir_name = data
|
||||
.path
|
||||
.components()
|
||||
.last()
|
||||
.unwrap()
|
||||
.as_os_str()
|
||||
.to_string_lossy();
|
||||
let default_template = data
|
||||
.index
|
||||
.default_template
|
||||
.to_owned()
|
||||
.unwrap_or(dir_name.into());
|
||||
|
||||
future::try_join_all(
|
||||
data.pages
|
||||
.into_iter()
|
||||
.map(|page| self.render_page(tera, default_template.clone(), page)),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
async fn render_page(
|
||||
&self,
|
||||
tera: &Tera,
|
||||
default_template: String,
|
||||
page_path: PathBuf,
|
||||
) -> Result<()> {
|
||||
tracing::debug!("Rendering {page_path:?}");
|
||||
|
||||
let page = load_page(&page_path).await?;
|
||||
let mut context = TeraContext::new();
|
||||
let mut template_name = default_template;
|
||||
let mut style_name = template_name.to_owned();
|
||||
|
||||
match page {
|
||||
crate::data::Page::Data(data) => {
|
||||
if let Some(tmpl) = data.metadata.template {
|
||||
template_name = tmpl.to_owned();
|
||||
style_name = tmpl;
|
||||
}
|
||||
context.insert("data", &data.data);
|
||||
}
|
||||
crate::data::Page::Content(content) => context.insert("content", &content),
|
||||
}
|
||||
{
|
||||
let mut styles = self.styles.lock().await;
|
||||
let style_embed = styles
|
||||
.get_style_embed(&style_name, &self.ctx.output_dir)
|
||||
.await?;
|
||||
context.insert("style", &style_embed);
|
||||
};
|
||||
|
||||
tracing::debug!("context = {context:?}");
|
||||
|
||||
let html = tera
|
||||
.render(&format!("{template_name}.html"), &context)
|
||||
.into_diagnostic()?;
|
||||
let rel_path = page_path
|
||||
.strip_prefix(&self.ctx.content_dir)
|
||||
.into_diagnostic()?;
|
||||
let mut out_path = self.ctx.output_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(())
|
||||
}
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use async_walkdir::WalkDir;
|
||||
use futures::StreamExt;
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
use rsass::output::Format;
|
||||
use tokio::fs;
|
||||
|
||||
const DEFAULT_SHEET_NAME: &str = "style";
|
||||
const EMBED_THRESHOLD: usize = 512;
|
||||
|
||||
pub struct Stylesheets {
|
||||
page_styles: HashMap<String, PathBuf>,
|
||||
processed_styles: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace")]
|
||||
pub async fn load_stylesheets(base_dir: &PathBuf) -> Result<Stylesheets> {
|
||||
let mut entries = WalkDir::new(base_dir);
|
||||
let mut page_styles = HashMap::new();
|
||||
let empty_path = PathBuf::new();
|
||||
|
||||
while let Some(res) = entries.next().await {
|
||||
match res {
|
||||
Ok(entry) => {
|
||||
let entry_path = entry.path();
|
||||
if entry_path.is_file() {
|
||||
let rel_path = entry_path.strip_prefix(base_dir).into_diagnostic()?;
|
||||
|
||||
if let Some(file_name) = entry_path.file_stem() {
|
||||
let file_name = rel_path.parent().unwrap_or(&empty_path).join(file_name);
|
||||
let file_name = file_name.to_string_lossy().into_owned();
|
||||
page_styles.insert(file_name, entry_path.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => return Err(e).into_diagnostic(),
|
||||
}
|
||||
}
|
||||
tracing::debug!("Styles {page_styles:?}");
|
||||
|
||||
Ok(Stylesheets {
|
||||
page_styles,
|
||||
processed_styles: HashMap::new(),
|
||||
})
|
||||
}
|
||||
|
||||
impl Stylesheets {
|
||||
#[tracing::instrument(level = "trace", skip(self, out_dir))]
|
||||
pub async fn get_style_embed(&mut self, name: &str, out_dir: &Path) -> Result<String> {
|
||||
let mut styles: Vec<String> = Vec::with_capacity(2);
|
||||
|
||||
if let Some(default_style) = self
|
||||
.get_processed_style(DEFAULT_SHEET_NAME, out_dir)
|
||||
.await?
|
||||
{
|
||||
styles.push(default_style);
|
||||
}
|
||||
if let Some(style) = self.get_processed_style(name, out_dir).await? {
|
||||
styles.push(style);
|
||||
}
|
||||
|
||||
Ok(styles.join(""))
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(self, out_dir))]
|
||||
async fn get_processed_style(&mut self, name: &str, out_dir: &Path) -> Result<Option<String>> {
|
||||
if let Some(processed) = self.processed_styles.get(name) {
|
||||
Ok(Some(processed.to_owned()))
|
||||
} else if let Some(source) = self.page_styles.get(name) {
|
||||
let format = Format {
|
||||
style: rsass::output::Style::Compressed,
|
||||
..Default::default()
|
||||
};
|
||||
let style_contents = rsass::compile_scss_path(source, format).into_diagnostic()?;
|
||||
let style_html = if style_contents.len() < EMBED_THRESHOLD {
|
||||
let utf_contents = String::from_utf8(style_contents).into_diagnostic()?;
|
||||
|
||||
format!(r#"<style type="text/css">{utf_contents}</style>"#)
|
||||
} else {
|
||||
let output_path = out_dir.join(name).with_extension("css");
|
||||
let parent = output_path.parent().unwrap();
|
||||
if !parent.exists() {
|
||||
fs::create_dir_all(parent).await.into_diagnostic()?;
|
||||
}
|
||||
fs::write(output_path, style_contents)
|
||||
.await
|
||||
.into_diagnostic()?;
|
||||
|
||||
format!(r#"<link rel="stylesheet" href="/{name}.css">"#)
|
||||
};
|
||||
self.processed_styles
|
||||
.insert(name.to_owned(), style_html.to_owned());
|
||||
|
||||
Ok(Some(style_html))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue