From c77cc9b170f59069e607b788f422a6f1fcd80977 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Wed, 23 Aug 2017 19:17:24 +0900 Subject: [PATCH] First draft of theme support --- Cargo.lock | 3 + components/config/src/lib.rs | 3 + components/content/src/page.rs | 3 +- components/content/src/section.rs | 3 +- components/pagination/Cargo.toml | 1 + components/pagination/src/lib.rs | 4 +- components/site/src/lib.rs | 77 ++++++++++++++----- components/site/test_site/config.toml | 1 + .../test_site/themes/sample/sass/sample.scss | 3 + .../test_site/themes/sample/static/some.js | 0 .../themes/sample/templates/index.html | 1 + .../site/test_site/themes/sample/theme.toml | 3 + components/site/tests/site.rs | 5 +- components/taxonomies/Cargo.toml | 1 + components/taxonomies/src/lib.rs | 6 +- components/templates/src/lib.rs | 2 +- components/utils/Cargo.toml | 1 + components/utils/src/lib.rs | 2 + components/utils/src/templates.rs | 21 +++++ src/cmd/serve.rs | 4 +- 20 files changed, 114 insertions(+), 30 deletions(-) create mode 100644 components/site/test_site/themes/sample/sass/sample.scss create mode 100644 components/site/test_site/themes/sample/static/some.js create mode 100644 components/site/test_site/themes/sample/templates/index.html create mode 100644 components/site/test_site/themes/sample/theme.toml create mode 100644 components/utils/src/templates.rs diff --git a/Cargo.lock b/Cargo.lock index 3e9e8c6..40b8c6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,6 +4,7 @@ version = "0.1.0" dependencies = [ "errors 0.1.0", "tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "tera 0.10.9 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -775,6 +776,7 @@ dependencies = [ "serde 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)", "tera 0.10.9 (registry+https://github.com/rust-lang/crates.io-index)", + "utils 0.1.0", ] [[package]] @@ -1163,6 +1165,7 @@ dependencies = [ "serde_derive 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)", "slug 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "tera 0.10.9 (registry+https://github.com/rust-lang/crates.io-index)", + "utils 0.1.0", ] [[package]] diff --git a/components/config/src/lib.rs b/components/config/src/lib.rs index c74206f..141bccf 100644 --- a/components/config/src/lib.rs +++ b/components/config/src/lib.rs @@ -23,6 +23,8 @@ pub struct Config { /// Base URL of the site, the only required config argument pub base_url: String, + /// Theme to use + pub theme: Option, /// Title of the site. Defaults to None pub title: Option, /// Whether to highlight all code blocks found in markdown files. Defaults to false @@ -130,6 +132,7 @@ impl Default for Config { fn default() -> Config { Config { title: Some("".to_string()), + theme: None, base_url: "http://a-website.com/".to_string(), highlight_code: Some(true), highlight_theme: Some("base16-ocean-dark".to_string()), diff --git a/components/content/src/page.rs b/components/content/src/page.rs index c5040a3..65be257 100644 --- a/components/content/src/page.rs +++ b/components/content/src/page.rs @@ -12,6 +12,7 @@ use errors::{Result, ResultExt}; use config::Config; use utils::fs::{read_file, find_related_assets}; use utils::site::get_reading_analytics; +use utils::templates::render_template; use front_matter::{PageFrontMatter, InsertAnchor, split_page_content}; use rendering::{Context, Header, markdown_to_html}; @@ -154,7 +155,7 @@ impl Page { context.add("current_url", &self.permalink); context.add("current_path", &self.path); - tera.render(&tpl_name, &context) + render_template(&tpl_name, tera, &context, config.theme.clone()) .chain_err(|| format!("Failed to render page '{}'", self.file.path.display())) } } diff --git a/components/content/src/section.rs b/components/content/src/section.rs index e4b63e4..8ce0ea5 100644 --- a/components/content/src/section.rs +++ b/components/content/src/section.rs @@ -9,6 +9,7 @@ use config::Config; use front_matter::{SectionFrontMatter, split_section_content}; use errors::{Result, ResultExt}; use utils::fs::read_file; +use utils::templates::render_template; use rendering::{Context, Header, markdown_to_html}; use page::Page; @@ -113,7 +114,7 @@ impl Section { context.add("current_url", &self.permalink); context.add("current_path", &self.path); - tera.render(&tpl_name, &context) + render_template(&tpl_name, tera, &context, config.theme.clone()) .chain_err(|| format!("Failed to render section '{}'", self.file.path.display())) } diff --git a/components/pagination/Cargo.toml b/components/pagination/Cargo.toml index 198d467..e0e0e9a 100644 --- a/components/pagination/Cargo.toml +++ b/components/pagination/Cargo.toml @@ -11,6 +11,7 @@ serde_derive = "1.0" errors = { path = "../errors" } config = { path = "../config" } content = { path = "../content" } +utils = { path = "../utils" } [dev-dependencies] front_matter = { path = "../front_matter" } diff --git a/components/pagination/src/lib.rs b/components/pagination/src/lib.rs index 4daf1e2..2d8b2fc 100644 --- a/components/pagination/src/lib.rs +++ b/components/pagination/src/lib.rs @@ -5,6 +5,7 @@ extern crate tera; extern crate errors; extern crate config; extern crate content; +extern crate utils; #[cfg(test)] extern crate front_matter; @@ -16,6 +17,7 @@ use tera::{Tera, Context, to_value, Value}; use errors::{Result, ResultExt}; use config::Config; use content::{Page, Section}; +use utils::templates::render_template; /// A list of all the pages in the paginator with their index and links @@ -170,7 +172,7 @@ impl<'a> Paginator<'a> { context.add("current_path", &pager.path); context.add("paginator", &self.build_paginator_context(pager)); - tera.render(&self.section.get_template_name(), &context) + render_template(&self.section.get_template_name(), tera, &context, config.theme.clone()) .chain_err(|| format!("Failed to render pager {} of section '{}'", pager.index, self.section.file.path.display())) } } diff --git a/components/site/src/lib.rs b/components/site/src/lib.rs index 3b4a3cf..e832303 100644 --- a/components/site/src/lib.rs +++ b/components/site/src/lib.rs @@ -33,6 +33,7 @@ use sass_rs::{Options, compile_file}; use errors::{Result, ResultExt}; use config::{Config, get_config}; use utils::fs::{create_file, create_directory, ensure_directory_exists}; +use utils::templates::render_template; use content::{Page, Section, populate_previous_and_next_pages, sort_pages}; use templates::{GUTENBERG_TERA, global_fns, render_redirect_template}; use front_matter::{SortBy, InsertAnchor}; @@ -67,7 +68,7 @@ pub struct Site { pub tera: Tera, live_reload: bool, output_path: PathBuf, - static_path: PathBuf, + pub static_path: PathBuf, pub tags: Option, pub categories: Option, /// A map of all .md files (section and pages) and their permalink @@ -80,14 +81,29 @@ impl Site { /// Passing in a path is only used in tests pub fn new>(path: P, config_file: &str) -> Result { let path = path.as_ref(); + let config = get_config(path, config_file); let tpl_glob = format!("{}/{}", path.to_string_lossy().replace("\\", "/"), "templates/**/*.*ml"); let mut tera = Tera::new(&tpl_glob).chain_err(|| "Error parsing templates")?; tera.extend(&GUTENBERG_TERA)?; + if let Some(ref theme) = config.theme { + // Test that the {templates,static} folder exist for that theme + let theme_path = path.join("themes").join(theme); + if !theme_path.join("templates").exists() { + bail!("Theme `{}` is missing a templates folder", theme); + } + if !theme_path.join("static").exists() { + bail!("Theme `{}` is missing a static folder", theme); + } + let theme_tpl_glob = format!("{}/{}", path.to_string_lossy().replace("\\", "/"), "themes/**/*.html"); + let tera_themes = Tera::new(&theme_tpl_glob).chain_err(|| "Error parsing templates from themes")?; + tera.extend(&tera_themes)?; + } + let site = Site { base_path: path.to_path_buf(), - config: get_config(path, config_file), + config: config, pages: HashMap::new(), sections: HashMap::new(), tera: tera, @@ -242,7 +258,7 @@ impl Site { if render { let insert_anchor = self.find_parent_section_insert_anchor(&self.pages[&path].file.parent); - let mut page = self.pages.get_mut(&path).unwrap(); + let page = self.pages.get_mut(&path).unwrap(); page.render_markdown(&self.permalinks, &self.tera, &self.config, insert_anchor)?; } @@ -259,7 +275,7 @@ impl Site { let prev = self.sections.insert(section.file.path.clone(), section); if render { - let mut section = self.sections.get_mut(&path).unwrap(); + let section = self.sections.get_mut(&path).unwrap(); section.render_markdown(&self.permalinks, &self.tera, &self.config)?; } @@ -353,9 +369,9 @@ impl Site { html } - /// Copy static file to public directory. - pub fn copy_static_file>(&self, path: P) -> Result<()> { - let relative_path = path.as_ref().strip_prefix(&self.static_path).unwrap(); + /// Copy the file at the given path into the public folder + pub fn copy_static_file>(&self, path: P, base_path: &PathBuf) -> Result<()> { + let relative_path = path.as_ref().strip_prefix(base_path).unwrap(); let target_path = self.output_path.join(relative_path); if let Some(parent_directory) = target_path.parent() { create_dir_all(parent_directory)?; @@ -364,24 +380,36 @@ impl Site { Ok(()) } - /// Copy the content of the `static` folder into the `public` folder - pub fn copy_static_directory(&self) -> Result<()> { - for entry in WalkDir::new(&self.static_path).into_iter().filter_map(|e| e.ok()) { - let relative_path = entry.path().strip_prefix(&self.static_path).unwrap(); + /// Copy the content of the given folder into the `public` folder + fn copy_static_directory(&self, path: &PathBuf) -> Result<()> { + for entry in WalkDir::new(path).into_iter().filter_map(|e| e.ok()) { + let relative_path = entry.path().strip_prefix(path).unwrap(); let target_path = self.output_path.join(relative_path); - if entry.path().is_dir() { if !target_path.exists() { create_directory(&target_path)?; } } else { let entry_fullpath = self.base_path.join(entry.path()); - self.copy_static_file(entry_fullpath)?; + self.copy_static_file(entry_fullpath, path)?; } } Ok(()) } + /// Copy the main `static` folder and the theme `static` folder if a theme is used + pub fn copy_static_directories(&self) -> Result<()> { + // The user files will overwrite the theme files + if let Some(ref theme) = self.config.theme { + self.copy_static_directory( + &self.base_path.join("themes").join(theme).join("static") + )?; + } + self.copy_static_directory(&self.static_path)?; + + Ok(()) + } + /// Deletes the `public` directory if it exists pub fn clean(&self) -> Result<()> { if self.output_path.exists() { @@ -440,17 +468,24 @@ impl Site { self.render_categories()?; self.render_tags()?; + if let Some(ref theme) = self.config.theme { + let theme_path = self.base_path.join("themes").join(theme); + if theme_path.join("sass").exists() { + self.compile_sass(&theme_path)?; + } + } + if self.config.compile_sass.unwrap() { - self.compile_sass()?; + self.compile_sass(&self.base_path)?; } - self.copy_static_directory() + self.copy_static_directories() } - pub fn compile_sass(&self) -> Result<()> { + pub fn compile_sass(&self, base_path: &PathBuf) -> Result<()> { ensure_directory_exists(&self.output_path)?; - let base_path = self.base_path.to_string_lossy().replace("\\", "/"); + let base_path = base_path.to_string_lossy().replace("\\", "/"); let sass_glob = format!("{}/{}", base_path, "sass/**/*.scss"); let files = glob(&sass_glob) .unwrap() @@ -495,7 +530,7 @@ impl Site { ensure_directory_exists(&self.output_path)?; create_file( &self.output_path.join("robots.txt"), - &self.tera.render("robots.txt", &Context::new())? + &render_template("robots.txt", &self.tera, &Context::new(), self.config.theme.clone())? ) } @@ -582,7 +617,7 @@ impl Site { } context.add("tags", &tags); - let sitemap = self.tera.render("sitemap.xml", &context)?; + let sitemap = &render_template("sitemap.xml", &self.tera, &context, self.config.theme.clone())?; create_file(&self.output_path.join("sitemap.xml"), &sitemap)?; @@ -616,9 +651,9 @@ impl Site { }; context.add("feed_url", &rss_feed_url); - let sitemap = self.tera.render("rss.xml", &context)?; + let feed = &render_template("rss.xml", &self.tera, &context, self.config.theme.clone())?; - create_file(&self.output_path.join("rss.xml"), &sitemap)?; + create_file(&self.output_path.join("rss.xml"), &feed)?; Ok(()) } diff --git a/components/site/test_site/config.toml b/components/site/test_site/config.toml index a9d8d86..4b1caed 100644 --- a/components/site/test_site/config.toml +++ b/components/site/test_site/config.toml @@ -4,6 +4,7 @@ highlight_code = true compile_sass = true generate_rss = true rss_limit = 2 +theme = "sample" [extra.author] name = "Vincent Prouillet" diff --git a/components/site/test_site/themes/sample/sass/sample.scss b/components/site/test_site/themes/sample/sass/sample.scss new file mode 100644 index 0000000..721f012 --- /dev/null +++ b/components/site/test_site/themes/sample/sass/sample.scss @@ -0,0 +1,3 @@ +body { + font-weight: bold; +} diff --git a/components/site/test_site/themes/sample/static/some.js b/components/site/test_site/themes/sample/static/some.js new file mode 100644 index 0000000..e69de29 diff --git a/components/site/test_site/themes/sample/templates/index.html b/components/site/test_site/themes/sample/templates/index.html new file mode 100644 index 0000000..e965047 --- /dev/null +++ b/components/site/test_site/themes/sample/templates/index.html @@ -0,0 +1 @@ +Hello diff --git a/components/site/test_site/themes/sample/theme.toml b/components/site/test_site/themes/sample/theme.toml new file mode 100644 index 0000000..1daa4fc --- /dev/null +++ b/components/site/test_site/themes/sample/theme.toml @@ -0,0 +1,3 @@ +name = "sample" + +[extra] diff --git a/components/site/tests/site.rs b/components/site/tests/site.rs index aa050ad..bf1d4e2 100644 --- a/components/site/tests/site.rs +++ b/components/site/tests/site.rs @@ -100,7 +100,6 @@ fn can_build_site_without_live_reload() { site.build().unwrap(); assert!(Path::new(&public).exists()); - assert!(file_exists!(public, "index.html")); assert!(file_exists!(public, "sitemap.xml")); assert!(file_exists!(public, "robots.txt")); @@ -130,6 +129,10 @@ fn can_build_site_without_live_reload() { assert_eq!(file_exists!(public, "categories/index.html"), false); assert_eq!(file_exists!(public, "tags/index.html"), false); + // Theme files are there + assert!(file_exists!(public, "sample.css")); + assert!(file_exists!(public, "some.js")); + // no live reload code assert_eq!(file_contains!(public, "index.html", "/livereload.js?port=1112&mindelay=10"), false); diff --git a/components/taxonomies/Cargo.toml b/components/taxonomies/Cargo.toml index 2bcd82c..a3daecd 100644 --- a/components/taxonomies/Cargo.toml +++ b/components/taxonomies/Cargo.toml @@ -13,3 +13,4 @@ errors = { path = "../errors" } config = { path = "../config" } content = { path = "../content" } front_matter = { path = "../front_matter" } +utils = { path = "../utils" } diff --git a/components/taxonomies/src/lib.rs b/components/taxonomies/src/lib.rs index a9c998f..ec8287f 100644 --- a/components/taxonomies/src/lib.rs +++ b/components/taxonomies/src/lib.rs @@ -7,6 +7,7 @@ extern crate errors; extern crate config; extern crate content; extern crate front_matter; +extern crate utils; use std::collections::HashMap; @@ -17,6 +18,7 @@ use config::Config; use errors::{Result, ResultExt}; use content::{Page, sort_pages}; use front_matter::SortBy; +use utils::templates::render_template; #[derive(Debug, Copy, Clone, PartialEq)] @@ -139,7 +141,7 @@ impl Taxonomy { context.add("current_url", &config.make_permalink(&format!("{}/{}", name, item.slug))); context.add("current_path", &format!("/{}/{}", name, item.slug)); - tera.render(&format!("{}.html", name), &context) + render_template(&format!("{}.html", name), tera, &context, config.theme.clone()) .chain_err(|| format!("Failed to render {} page.", name)) } @@ -151,7 +153,7 @@ impl Taxonomy { context.add("current_url", &config.make_permalink(&name)); context.add("current_path", &name); - tera.render(&format!("{}.html", name), &context) + render_template(&format!("{}.html", name), tera, &context, config.theme.clone()) .chain_err(|| format!("Failed to render {} page.", name)) } } diff --git a/components/templates/src/lib.rs b/components/templates/src/lib.rs index 180a871..398cf9b 100644 --- a/components/templates/src/lib.rs +++ b/components/templates/src/lib.rs @@ -14,8 +14,8 @@ pub mod filters; pub mod global_fns; use tera::{Tera, Context}; -use errors::{Result, ResultExt}; +use errors::{Result, ResultExt}; lazy_static! { pub static ref GUTENBERG_TERA: Tera = { diff --git a/components/utils/Cargo.toml b/components/utils/Cargo.toml index d93de9f..24c3aeb 100644 --- a/components/utils/Cargo.toml +++ b/components/utils/Cargo.toml @@ -5,6 +5,7 @@ authors = ["Vincent Prouillet "] [dependencies] errors = { path = "../errors" } +tera = "0.10" [dev-dependencies] diff --git a/components/utils/src/lib.rs b/components/utils/src/lib.rs index 565bbdb..6b447f2 100644 --- a/components/utils/src/lib.rs +++ b/components/utils/src/lib.rs @@ -3,6 +3,8 @@ extern crate errors; #[cfg(test)] extern crate tempdir; +extern crate tera; pub mod fs; pub mod site; +pub mod templates; diff --git a/components/utils/src/templates.rs b/components/utils/src/templates.rs new file mode 100644 index 0000000..b75566c --- /dev/null +++ b/components/utils/src/templates.rs @@ -0,0 +1,21 @@ +use tera::{Tera, Context}; + +use errors::Result; + +/// Renders the given template with the given context, but also ensures that, if the default file +/// is not found, it will look up for the equivalent template for the current theme if there is one +pub fn render_template(name: &str, tera: &Tera, context: &Context, theme: Option) -> Result { + if tera.templates.contains_key(name) { + return tera + .render(name, context) + .map_err(|e| e.into()); + } + + if let Some(ref t) = theme { + return tera + .render(&format!("{}/templates/{}", t, name), context) + .map_err(|e| e.into()); + } + + bail!("Tried to render `{}` but the template wasn't found", name) +} diff --git a/src/cmd/serve.rs b/src/cmd/serve.rs index 18804e3..6107e81 100644 --- a/src/cmd/serve.rs +++ b/src/cmd/serve.rs @@ -180,12 +180,12 @@ pub fn serve(interface: &str, port: &str, config_file: &str) -> Result<()> { (ChangeKind::StaticFiles, p) => { if path.is_file() { console::info(&format!("-> Static file changes detected {}", path.display())); - rebuild_done_handling(&broadcaster, site.copy_static_file(&path), &p); + rebuild_done_handling(&broadcaster, site.copy_static_file(&path, &site.static_path), &p); } }, (ChangeKind::Sass, p) => { console::info(&format!("-> Sass file changed {}", path.display())); - rebuild_done_handling(&broadcaster, site.compile_sass(), &p); + rebuild_done_handling(&broadcaster, site.compile_sass(&site.base_path), &p); }, }; console::report_elapsed_time(start);