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