From bca5f990c1bb85e3c7796fee47fd22114fb1802e Mon Sep 17 00:00:00 2001
From: clearloop <26088946+clearloop@users.noreply.github.com>
Date: Sat, 30 Dec 2023 17:31:08 +0800
Subject: [PATCH] feat(app): render page index (#2)
* feat(manifest): introduce out directory
* feat(manifest): introduce public directory
* feat(app): render app
* feat(app): embed layout
* feat(app): embed css in the output site
* chore(app): improve the default paths of manifest
---
.gitignore | 1 +
Cargo.lock | 1 +
Cargo.toml | 1 +
blog/cydonia.toml | 2 +
blog/templates/index.hbs | 8 +++-
blog/templates/layout.hbs | 17 ++++++---
blog/theme.css | 6 +++
src/app.rs | 79 ++++++++++++++++++++++++++++++++++-----
src/manifest.rs | 78 ++++++++++++++++++++++++++++++--------
src/theme.rs | 23 ++++++++----
src/utils.rs | 23 ++++++++++--
tests/main.rs | 24 +++++-------
12 files changed, 203 insertions(+), 60 deletions(-)
diff --git a/.gitignore b/.gitignore
index 585b6fc..0ff116a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+out
/target
.log
.DS_Store
diff --git a/Cargo.lock b/Cargo.lock
index e794be5..7daa2d3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -324,6 +324,7 @@ dependencies = [
"notify",
"pulldown-cmark",
"serde",
+ "serde_json",
"serde_yaml",
"toml",
"tracing",
diff --git a/Cargo.toml b/Cargo.toml
index e42fcef..111c51a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -16,6 +16,7 @@ handlebars = { version = "4.5.0", features = [ "dir_source" ] }
notify = "6.1.1"
pulldown-cmark = { version = "0.9.3", default-features = false }
serde = { version = "1.0.193", features = [ "derive" ] }
+serde_json = "1.0.108"
serde_yaml = "0.9.29"
toml = "0.8.8"
tracing = "0.1.40"
diff --git a/blog/cydonia.toml b/blog/cydonia.toml
index cb62935..b2bc606 100644
--- a/blog/cydonia.toml
+++ b/blog/cydonia.toml
@@ -5,7 +5,9 @@ name = "Cydonia" # The name of the site.
# --------------------------------------
favicon = "favicon.ico" # The path to the favicon.ico.
+out = "out" # The path to the output directory.
posts = "posts" # The path to the posts.
+public = "public" # The path to the public directory.
templates = "templates" # The path to the templates.
# Theme could also be a folder:
diff --git a/blog/templates/index.hbs b/blog/templates/index.hbs
index 41ec760..1778ace 100644
--- a/blog/templates/index.hbs
+++ b/blog/templates/index.hbs
@@ -1,3 +1,7 @@
-
+{{#*inline "page"}}
+
+ {{ name }}
+
+{{/inline}}
-
+{{> layout }}
diff --git a/blog/templates/layout.hbs b/blog/templates/layout.hbs
index 7d94f17..ba6a3f4 100644
--- a/blog/templates/layout.hbs
+++ b/blog/templates/layout.hbs
@@ -1,14 +1,19 @@
+ {{ name }}
- {{ title }}
-
- {{> headers }}
+
+ {{#if index}}
+
+ {{/if}}
+ {{#if post}}
+
+ {{/if}}
- {{> nav }}
- {{ ~>page }}
- {{> footer }}
+ {{> page }}
+
diff --git a/blog/theme.css b/blog/theme.css
index e69de29..83556d2 100644
--- a/blog/theme.css
+++ b/blog/theme.css
@@ -0,0 +1,6 @@
+html,
+body {
+ min-height: 100%;
+ background-color: #000;
+ color: #fefefe;
+}
diff --git a/src/app.rs b/src/app.rs
index 677086c..2be1568 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -11,22 +11,81 @@
//! - theme.css
//! ```
-use crate::Manifest;
+use crate::{Manifest, Post, Theme};
use anyhow::Result;
-use std::path::PathBuf;
+use handlebars::Handlebars;
+use std::{
+ fs::{self, File},
+ path::{Path, PathBuf},
+};
/// The root of the site.
-pub struct App {
- /// The root path of the resources.
- pub root: PathBuf,
- /// The manifest of the site.
+pub struct App<'app> {
+ /// The handlebars instance.
+ pub handlebars: Handlebars<'app>,
+ /// The cydonia.toml manifest.
pub manifest: Manifest,
+ /// The posts.
+ pub posts: Vec,
+ /// The theme.
+ pub theme: Theme,
}
-impl App {
+impl<'app> TryFrom for App<'app> {
+ type Error = anyhow::Error;
+
+ fn try_from(manifest: Manifest) -> Result {
+ Ok(Self {
+ handlebars: manifest.handlebars()?,
+ posts: manifest.posts()?,
+ theme: manifest.theme()?,
+ manifest,
+ })
+ }
+}
+
+impl<'app> App<'app> {
/// Create a new app.
- pub fn new(root: PathBuf) -> Result {
- let manifest = Manifest::load(&root)?;
- Ok(Self { root, manifest })
+ pub fn load(root: PathBuf) -> Result {
+ Manifest::load(root)?.try_into()
+ }
+
+ /// Render the site.
+ pub fn render(&self) -> Result<()> {
+ fs::create_dir_all(&self.manifest.out)?;
+ self.manifest.copy_public()?;
+ self.render_css()?;
+ self.render_index()?;
+ Ok(())
+ }
+
+ /// Write css to the output directory.
+ pub fn render_css(&self) -> Result<()> {
+ fs::write(self.manifest.out.join("index.css"), &self.theme.index)?;
+ fs::write(self.manifest.out.join("post.css"), &self.theme.post).map_err(Into::into)
+ }
+
+ /// Render the index page.
+ pub fn render_index(&self) -> Result<()> {
+ self.render_template(
+ self.manifest.out.join("index.html"),
+ "index",
+ serde_json::json!({
+ "name": self.manifest.name,
+ "index": true,
+ }),
+ )
+ }
+
+ /// Render a template.
+ pub fn render_template(
+ &self,
+ name: impl AsRef,
+ template: &str,
+ data: serde_json::Value,
+ ) -> Result<()> {
+ let file = File::create(self.manifest.out.join(name.as_ref()))?;
+ self.handlebars.render_to_write(template, &data, file)?;
+ Ok(())
}
}
diff --git a/src/manifest.rs b/src/manifest.rs
index 9cfa176..a66c366 100644
--- a/src/manifest.rs
+++ b/src/manifest.rs
@@ -1,6 +1,9 @@
//! Manifest of the site.
-use crate::{utils::Read, Post};
+use crate::{
+ utils::{Prefix, Read},
+ Post, Theme,
+};
use anyhow::Result;
use handlebars::Handlebars;
use serde::{Deserialize, Serialize};
@@ -16,20 +19,29 @@ pub struct Manifest {
pub name: String,
/// The path to the favicon.
- favicon: Option,
+ #[serde(default = "default::favicon")]
+ pub favicon: PathBuf,
+
+ /// The output directory.
+ #[serde(default = "default::out")]
+ pub out: PathBuf,
/// The path of the posts.
- #[serde(default = "Manifest::default_posts")]
+ #[serde(default = "default::posts")]
pub posts: PathBuf,
+ /// The path of the public directory.
+ #[serde(default = "default::public")]
+ pub public: PathBuf,
+
/// The path of the templates.
- #[serde(default = "Manifest::default_templates")]
+ #[serde(default = "default::templates")]
pub templates: PathBuf,
/// The path of the theme.
///
/// Could be a file or a directory.
- #[serde(default = "Manifest::default_theme")]
+ #[serde(default = "default::theme")]
pub theme: PathBuf,
}
@@ -37,18 +49,19 @@ impl Manifest {
/// Load manifest from the provided path.
pub fn load(root: impl AsRef) -> Result {
let path = root.as_ref().join("cydonia.toml");
- let mut manifest: Self = toml::from_str(&root.as_ref().join("cydonia.toml").read()?)
+ let manifest: Self = toml::from_str(&root.as_ref().join("cydonia.toml").read()?)
.map_err(|e| anyhow::anyhow!("Failed to parse {}: {}", path.display(), e))?;
- if manifest.posts.is_relative() {
- manifest.posts = root.as_ref().join(&manifest.posts);
- }
+ Ok(manifest.abs(root))
+ }
- if manifest.templates.is_relative() {
- manifest.templates = root.as_ref().join(&manifest.templates);
+ /// Copy the public directory.
+ pub fn copy_public(&self) -> Result<()> {
+ if self.public.exists() {
+ std::fs::copy(&self.public, self.out.join("public"))?;
}
- Ok(manifest)
+ Ok(())
}
/// Get the posts.
@@ -67,18 +80,53 @@ impl Manifest {
Ok(handlebars)
}
+ /// Get the theme.
+ pub fn theme(&self) -> Result {
+ Theme::load(&self.theme)
+ }
+
+ /// Make paths absolute.
+ fn abs(mut self, prefix: impl AsRef) -> Self {
+ self.favicon.prefix(&prefix);
+ self.out.prefix(&prefix);
+ self.posts.prefix(&prefix);
+ self.public.prefix(&prefix);
+ self.templates.prefix(&prefix);
+ self.theme.prefix(&prefix);
+ self
+ }
+}
+
+mod default {
+ use std::{fs, path::PathBuf};
+
+ /// Default implementation of the out directory.
+ pub fn favicon() -> PathBuf {
+ fs::canonicalize(PathBuf::from("favicon")).unwrap_or_default()
+ }
+
+ /// Default implementation of the out directory.
+ pub fn out() -> PathBuf {
+ fs::canonicalize(PathBuf::from("out")).unwrap_or_default()
+ }
+
/// Default implementation of the posts.
- pub fn default_posts() -> PathBuf {
+ pub fn posts() -> PathBuf {
fs::canonicalize(PathBuf::from("posts")).unwrap_or_default()
}
+ /// Default implementation of the posts.
+ pub fn public() -> PathBuf {
+ fs::canonicalize(PathBuf::from("public")).unwrap_or_default()
+ }
+
/// Default implementation of the templates.
- pub fn default_templates() -> PathBuf {
+ pub fn templates() -> PathBuf {
fs::canonicalize(PathBuf::from("templates")).unwrap_or_default()
}
/// Default implementation of the templates.
- pub fn default_theme() -> PathBuf {
+ pub fn theme() -> PathBuf {
fs::canonicalize(PathBuf::from("theme.css")).unwrap_or_default()
}
}
diff --git a/src/theme.rs b/src/theme.rs
index be633b9..bdacaba 100644
--- a/src/theme.rs
+++ b/src/theme.rs
@@ -2,10 +2,10 @@
use crate::utils::Read;
use anyhow::Result;
-use std::path::PathBuf;
+use std::path::Path;
/// The theme for the site.
-#[derive(Debug, Clone)]
+#[derive(Debug, Default, Clone)]
pub struct Theme {
/// Styles for the index page.
pub index: String,
@@ -15,8 +15,11 @@ pub struct Theme {
impl Theme {
/// Loads theme from the given path.
- pub fn load(path: PathBuf) -> Result {
- if path.is_file() {
+ pub fn load(path: impl AsRef) -> Result {
+ let path = path.as_ref();
+ if !path.exists() {
+ Ok(Default::default())
+ } else if path.is_file() {
let theme = path.read()?;
Ok(Self {
@@ -25,10 +28,14 @@ impl Theme {
})
} else {
let theme = path.join("theme.css").read().unwrap_or_default();
-
- let index = [theme.clone(), path.join("index.css").read()?].concat();
- let post = [theme, path.join("post.css").read()?].concat();
- Ok(Self { index, post })
+ Ok(Self {
+ index: [
+ theme.clone(),
+ path.join("index.css").read().unwrap_or_default(),
+ ]
+ .concat(),
+ post: [theme, path.join("post.css").read().unwrap_or_default()].concat(),
+ })
}
}
}
diff --git a/src/utils.rs b/src/utils.rs
index cfef83c..bea04ac 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -2,10 +2,11 @@
use anyhow::Result;
use colored::Colorize;
-use std::path::Path;
+use std::path::{Path, PathBuf};
/// A trait for reading file with full error info.
-pub trait Read {
+pub trait Read: Sized {
+ /// Read self to string with proper error info.
fn read(&self) -> Result;
}
@@ -17,10 +18,24 @@ where
let path = self.as_ref();
std::fs::read_to_string(path).map_err(|e| {
anyhow::anyhow!(
- "Failed to read file: {}, error: {}",
- path.display().to_string().dimmed().underline(),
+ "Failed to read file: {}, {}",
+ path.display().to_string().underline(),
e.to_string()
)
})
}
}
+
+/// Extension trait for `PathBuf`.
+pub trait Prefix {
+ /// Prefix self with another path.
+ fn prefix(&mut self, prefix: impl AsRef);
+}
+
+impl Prefix for PathBuf {
+ fn prefix(&mut self, prefix: impl AsRef) {
+ if self.is_relative() {
+ *self = prefix.as_ref().join(&self)
+ }
+ }
+}
diff --git a/tests/main.rs b/tests/main.rs
index 1b3798d..bf91bd6 100644
--- a/tests/main.rs
+++ b/tests/main.rs
@@ -1,40 +1,34 @@
//! Main tests for cydonia.
use anyhow::Result;
-use cydonia::{App, Post};
+use cydonia::{App, Manifest, Post};
use std::path::PathBuf;
-fn blog() -> Result {
- App::new(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("blog"))
+fn manifest() -> Result {
+ Manifest::load(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("blog"))
}
#[test]
-fn app() -> Result<()> {
- blog()?;
-
+fn render() -> Result<()> {
+ let app: App<'_> = manifest()?.try_into()?;
+ app.render()?;
Ok(())
}
#[test]
fn handlebars() -> Result<()> {
- let app = blog()?;
- app.manifest.handlebars()?;
-
+ manifest()?.handlebars()?;
Ok(())
}
#[test]
fn post() -> Result<()> {
- let app = blog()?;
- Post::load(app.manifest.posts.join("2023-12-29-hello-world.md"))?;
-
+ Post::load(manifest()?.posts.join("2023-12-29-hello-world.md"))?;
Ok(())
}
#[test]
fn posts() -> Result<()> {
- let app = blog()?;
- app.manifest.posts()?;
-
+ manifest()?.posts()?;
Ok(())
}