diff --git a/example/content/2024-10-19-what-is-marmite-static-site-generator.md b/example/content/2024-10-19-what-is-marmite-static-site-generator.md index 0d5f5a3..7de57a2 100644 --- a/example/content/2024-10-19-what-is-marmite-static-site-generator.md +++ b/example/content/2024-10-19-what-is-marmite-static-site-generator.md @@ -211,8 +211,10 @@ Other options are available and can be viewed on [repository](https://github.com ## Theme customization The embedded templates are created with [picocss.com](https://picocss.com/) and -it is easy to customize, just put a `style.css` in the same folder where the markdown -files are located and use anything that pico supports or just be creative with css. +it is easy to customize, just put a `custom.css` and a `custom.js` +in your root folder and use anything that pico supports or just be creative with css. + +If customizing the css and js is not enough then you can create your own theme. ## Creating a new Theme diff --git a/example/static/style.css b/example/static/custom.css similarity index 100% rename from example/static/style.css rename to example/static/custom.css diff --git a/example/static/custom.js b/example/static/custom.js new file mode 100644 index 0000000..638fbb0 --- /dev/null +++ b/example/static/custom.js @@ -0,0 +1 @@ +/* Customize me */ diff --git a/example/static/marmite.css b/example/static/marmite.css index 84d0376..a9990c6 100644 --- a/example/static/marmite.css +++ b/example/static/marmite.css @@ -121,3 +121,7 @@ article footer { justify-content: space-between; align-items: center; } + +.spoiler, .spoiler > * { transition: color 0.5s, opacity 0.5s } +.spoiler:not(:hover) { color: transparent;background-color: #808080} +.spoiler:not(:hover) > * { opacity: 0 } diff --git a/example/static/marmite.js b/example/static/marmite.js new file mode 100644 index 0000000..b8c31bc --- /dev/null +++ b/example/static/marmite.js @@ -0,0 +1,90 @@ +// Theme switcher - light/dark +const themeSwitcher = { + // Config + _scheme: "auto", + toggleButton: document.getElementById("theme-toggle"), + rootAttribute: "data-theme", + localStorageKey: "picoPreferredColorScheme", + + // Init + init() { + this.scheme = this.schemeFromLocalStorage; + this.initToggle(); + this.updateIcon(); + }, + + // Get color scheme from local storage + get schemeFromLocalStorage() { + return window.localStorage?.getItem(this.localStorageKey) ?? this._scheme; + }, + + // Preferred color scheme + get preferredColorScheme() { + return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; + }, + + // Init toggle + initToggle() { + this.toggleButton.addEventListener( + "click", + (event) => { + event.preventDefault(); + // Toggle scheme + this.scheme = this.scheme === "dark" ? "light" : "dark"; + this.updateIcon(); + }, + false + ); + }, + + // Set scheme + set scheme(scheme) { + if (scheme == "auto") { + this._scheme = this.preferredColorScheme; + } else if (scheme == "dark" || scheme == "light") { + this._scheme = scheme; + } + this.applyScheme(); + this.schemeToLocalStorage(); + }, + + // Get scheme + get scheme() { + return this._scheme; + }, + + // Apply scheme + applyScheme() { + document.querySelector("html")?.setAttribute(this.rootAttribute, this.scheme); + // staticBase is defined on content.html template + document.querySelector("#highlightjs-theme")?.setAttribute("href", `${staticBase}/github-${this.scheme}.min.css`); + }, + + // Store scheme to local storage + schemeToLocalStorage() { + window.localStorage?.setItem(this.localStorageKey, this.scheme); + }, + + // Update icon based on the current scheme + updateIcon() { + if (this.scheme === "dark") { + this.toggleButton.innerHTML = "☼"; // Sun icon for light mode + } else { + this.toggleButton.innerHTML = "☽"; // Moon icon for dark mode + } + }, +}; + +// Init +themeSwitcher.init(); + +// Menu + +const menuToggle = document.getElementById('menu-toggle'); +const headerMenu = document.getElementById('header-menu'); + +menuToggle.addEventListener('click', function () { + headerMenu.classList.toggle('active'); +}); + + diff --git a/example/static/menu.js b/example/static/menu.js deleted file mode 100644 index c087e49..0000000 --- a/example/static/menu.js +++ /dev/null @@ -1,7 +0,0 @@ - -const menuToggle = document.getElementById('menu-toggle'); -const headerMenu = document.getElementById('header-menu'); - -menuToggle.addEventListener('click', function () { - headerMenu.classList.toggle('active'); -}); \ No newline at end of file diff --git a/example/static/robots.txt b/example/static/robots.txt new file mode 100644 index 0000000..5e9db7d --- /dev/null +++ b/example/static/robots.txt @@ -0,0 +1,5 @@ +User-agent: Mediapartners-Google +Disallow: + +User-agent: * +Allow: / diff --git a/example/static/theme-switcher.js b/example/static/theme-switcher.js deleted file mode 100644 index 58c8b73..0000000 --- a/example/static/theme-switcher.js +++ /dev/null @@ -1,84 +0,0 @@ -/*! - * Minimal theme switcher - * - * Pico.css - https://picocss.com - * Copyright 2019-2024 - Licensed under MIT - */ -const themeSwitcher = { - // Config - _scheme: "auto", - toggleButton: document.getElementById("theme-toggle"), - rootAttribute: "data-theme", - localStorageKey: "picoPreferredColorScheme", - - // Init - init() { - this.scheme = this.schemeFromLocalStorage; - this.initToggle(); - this.updateIcon(); - }, - - // Get color scheme from local storage - get schemeFromLocalStorage() { - return window.localStorage?.getItem(this.localStorageKey) ?? this._scheme; - }, - - // Preferred color scheme - get preferredColorScheme() { - return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; - }, - - // Init toggle - initToggle() { - this.toggleButton.addEventListener( - "click", - (event) => { - event.preventDefault(); - // Toggle scheme - this.scheme = this.scheme === "dark" ? "light" : "dark"; - this.updateIcon(); - }, - false - ); - }, - - // Set scheme - set scheme(scheme) { - if (scheme == "auto") { - this._scheme = this.preferredColorScheme; - } else if (scheme == "dark" || scheme == "light") { - this._scheme = scheme; - } - this.applyScheme(); - this.schemeToLocalStorage(); - }, - - // Get scheme - get scheme() { - return this._scheme; - }, - - // Apply scheme - applyScheme() { - document.querySelector("html")?.setAttribute(this.rootAttribute, this.scheme); - // staticBase is defined on content.html template - document.querySelector("#highlightjs-theme")?.setAttribute("href", `${staticBase}/github-${this.scheme}.min.css`); - }, - - // Store scheme to local storage - schemeToLocalStorage() { - window.localStorage?.setItem(this.localStorageKey, this.scheme); - }, - - // Update icon based on the current scheme - updateIcon() { - if (this.scheme === "dark") { - this.toggleButton.innerHTML = "☼"; // Sun icon for light mode - } else { - this.toggleButton.innerHTML = "☽"; // Moon icon for dark mode - } - }, -}; - -// Init -themeSwitcher.init(); diff --git a/example/templates/base.html b/example/templates/base.html index c1c1770..92078f1 100644 --- a/example/templates/base.html +++ b/example/templates/base.html @@ -16,7 +16,7 @@ } - + {% endblock -%} @@ -62,8 +62,8 @@

{{ site.name }}

{%- block tail %} - - + + {% endblock -%} diff --git a/src/main.rs b/src/main.rs index d3a2cad..06688f3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,6 @@ mod config; mod content; mod embedded; mod markdown; -mod robots; mod server; mod site; mod tera_functions; diff --git a/src/robots.rs b/src/robots.rs deleted file mode 100644 index 0ca4956..0000000 --- a/src/robots.rs +++ /dev/null @@ -1,25 +0,0 @@ -use log::{error, info}; -use std::fs; -use std::path::Path; - -// Copy or create robots.txt -const ROBOTS_SRC: &str = "robots.txt"; -const DEFAULT_ROBOTS: &str = "User-agent: * -Disallow: /private -Allow: /public"; - -pub fn handle(content_dir: &Path, output_path: &Path) { - let robots_src = content_dir.join(ROBOTS_SRC); - let robots_dst = output_path.join(ROBOTS_SRC); - if robots_src.exists() { - if let Err(e) = fs::copy(&robots_src, &robots_dst) { - error!("Failed to copy robots.txt: {}", e); - } else { - info!("Copied robots.txt to output folder"); - } - } else if let Err(e) = fs::write(&robots_dst, DEFAULT_ROBOTS) { - error!("Failed to create default robots.txt: {}", e); - } else { - info!("Generated default robots.txt in output folder"); - } -} diff --git a/src/server.rs b/src/server.rs index ea44c3e..42cf820 100644 --- a/src/server.rs +++ b/src/server.rs @@ -3,7 +3,7 @@ use std::io::Cursor; use std::path::PathBuf; use std::sync::Arc; use std::{fs::File, path::Path}; -use tiny_http::{Response, Server}; +use tiny_http::{Header, Response, Server}; pub fn start(bind_address: &str, output_folder: &Arc) { let server = Server::http(bind_address).unwrap(); @@ -28,6 +28,7 @@ pub fn start(bind_address: &str, output_folder: &Arc) { } } +#[allow(clippy::case_sensitive_file_extension_comparisons)] fn handle_request( request: &tiny_http::Request, output_folder: &Path, @@ -50,7 +51,12 @@ fn handle_request( request_path, request.http_version() ); - Ok(Response::from_data(buffer)) + let mut resp = Response::from_data(buffer); + let js_header = Header::from_bytes("Content-Type", "text/javascript").unwrap(); + if request_path.ends_with(".js") { + resp.add_header(js_header); + } + Ok(resp) } Err(err) => { error!("Failed to read file {}: {}", file_path.display(), err); diff --git a/src/site.rs b/src/site.rs index 4fdc783..69bb226 100644 --- a/src/site.rs +++ b/src/site.rs @@ -2,9 +2,8 @@ use crate::config::Marmite; use crate::content::{check_for_duplicate_slugs, group_by_tags, slugify, Content}; use crate::embedded::{generate_static, EMBEDDED_TERA}; use crate::markdown::process_file; -use crate::robots; use crate::tera_functions::UrlFor; -use fs_extra::dir::{copy, CopyOptions}; +use fs_extra::dir::{copy as dircopy, CopyOptions}; use log::{debug, error, info}; use serde::Serialize; use std::path::Path; @@ -228,14 +227,13 @@ fn handle_static_artifacts( output_folder: &Arc, content_dir: &std::path::Path, ) { - robots::handle(content_dir, output_folder); - + // Copy static files let static_source = input_folder.join(site_data.site.static_path); if static_source.is_dir() { let mut options = CopyOptions::new(); options.overwrite = true; // Overwrite files if they already exist - if let Err(e) = copy(&static_source, &**output_folder, &options) { + if let Err(e) = dircopy(&static_source, &**output_folder, &options) { error!("Failed to copy static directory: {}", e); process::exit(1); } @@ -246,6 +244,7 @@ fn handle_static_artifacts( &output_folder.display() ); } else { + // generate from embedded generate_static(&output_folder.join(site_data.site.static_path)); } @@ -255,7 +254,7 @@ fn handle_static_artifacts( let mut options = CopyOptions::new(); options.overwrite = true; // Overwrite files if they already exist - if let Err(e) = copy(&media_source, &**output_folder, &options) { + if let Err(e) = dircopy(&media_source, &**output_folder, &options) { error!("Failed to copy media directory: {}", e); process::exit(1); } @@ -267,30 +266,38 @@ fn handle_static_artifacts( ); } - // Copy or create favicon.ico - let favicon_dst = output_folder.join("favicon.ico"); - - // Possible paths where favicon.ico might exist - let favicon_src_paths = [ - input_folder.join("static").join("favicon.ico"), // User's favicon.ico - // on #20 we may have embedded statics + // we want to check if the file exists in `input_folder` and `content_dir` + // if not then we want to check if exists in `output_folder/static` (came from embedded) + // the first we find we want to copy to the `output_folder/{destiny_path}` + let custom_files = [ + // name, destination + ("custom.css", site_data.site.static_path), + ("custom.js", site_data.site.static_path), + ("favicon.ico", ""), + ("robots.txt", ""), ]; - - for favicon_src in &favicon_src_paths { - if favicon_src.exists() { - match fs::copy(favicon_src, &favicon_dst) { - Ok(_) => { - info!( - "Copied favicon.ico from '{}' to output folder", - favicon_src.display() - ); - break; + let output_static_destiny = output_folder.join(site_data.site.static_path); + let possible_sources = [input_folder, content_dir, output_static_destiny.as_path()]; + let mut copied_custom_files = Vec::new(); + for possible_source in &possible_sources { + for custom_file in custom_files { + let source_file = possible_source.join(custom_file.0); + if copied_custom_files.contains(&custom_file.0.to_string()) { + continue; + } + if source_file.exists() { + let destiny_path = output_folder.join(custom_file.1).join(custom_file.0); + match fs::copy(&source_file, &destiny_path) { + Ok(_) => { + copied_custom_files.push(custom_file.0.to_string()); + info!( + "Copied {} to {}", + source_file.display(), + &destiny_path.display() + ); + } + Err(e) => error!("Failed to copy {}: {}", source_file.display(), e), } - Err(e) => error!( - "Failed to copy favicon.ico from '{}': {}", - favicon_src.display(), - e - ), } } }