diff --git a/Cargo.lock b/Cargo.lock index 0f855f2..34beb46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -101,6 +151,7 @@ version = "0.1.0" dependencies = [ "axum", "chrono", + "clap", "once_cell", "pulldown-cmark", "serde", @@ -150,6 +201,52 @@ dependencies = [ "windows-link", ] +[[package]] +name = "clap" +version = "4.5.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2cfd7bf8a6017ddaa4e32ffe7403d547790db06bd171c1c53926faab501623" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a4c05b9e80c5ccd3a7ef080ad7b6ba7d6fc00a985b8b157197075677c82c7a0" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -225,6 +322,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "http" version = "1.3.1" @@ -338,6 +441,12 @@ dependencies = [ "cc", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itoa" version = "1.0.15" @@ -429,6 +538,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "parking_lot" version = "0.12.5" @@ -631,6 +746,12 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "2.0.108" @@ -791,6 +912,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index 12461c5..0cffcfb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,3 +12,4 @@ pulldown-cmark = "0.12" serde = { version = "1.0", features = ["derive"] } chrono = "0.4" once_cell = "1.19" +clap = { version = "4.5", features = ["derive"] } diff --git a/lua/lemon.lua b/lua/lemon.lua new file mode 100644 index 0000000..15bef70 --- /dev/null +++ b/lua/lemon.lua @@ -0,0 +1,152 @@ +Lemon = { + ws = {}, + ws_file = '.nvim.workspace.lua', + term_buf = nil, + term_win_cmd = 'belowright 12split', +} + +function LoadWorkspace() + -- Load persistent configuration from .workspace.lua + local loaded, workspace = pcall(dofile, Lemon.ws_file) + if not loaded then return nil end + return workspace +end + +function WriteWorkspace() + -- A very minimal serializer for workspace configuration + local s = { l = "", ls = {}, i = "" } + local function w(v) s.l = s.l .. v end + local function nl() + s.ls[#s.ls + 1] = s.l; s.l = s.i; + end + local function wv(v) + local t = type(v) + if t == 'table' then + w('{'); local pi = s.i; s.i = s.i .. " " + for k1, v1 in pairs(v) do + nl(); w('['); wv(k1); w('] = '); wv(v1); w(',') + end + s.i = pi; nl(); w('}'); + elseif t == 'number' then + w(tostring(v)) + elseif t == 'string' then + w('"' .. v .. '"') + else + w(tostring(v)) + end + end + + -- Write the workspace file + w("return "); wv(Lemon.workspace); nl() + vim.fn.writefile(s.ls, Lemon.ws_file) +end + +-- Loads the workspace from the file, or return the default +---@param default table +---@return table +function InitWorkspace(default) + Lemon.ws = LoadWorkspace() + if Lemon.ws == nil then + Lemon.ws = default + end + return Lemon.ws +end + +function TermShow() + local info = GetTermInfo() + + if info == nil then + -- Create new terminal buffer + vim.cmd(Lemon.term_win_cmd) + vim.cmd('terminal') + Lemon.term_buf = vim.api.nvim_get_current_buf() + -- Mark buffer so we can identify it later + vim.api.nvim_buf_set_var(Lemon.term_buf, 'lemon_terminal', true) + info = GetTermInfo() + elseif info.win == nil then + -- Buffer exists but not visible, open it + vim.cmd(Lemon.term_win_cmd) + vim.api.nvim_win_set_buf(0, Lemon.term_buf) + else + -- Window is visible, switch to it + vim.api.nvim_set_current_win(info.win) + end +return info +end + +-- Find or create persistent terminal buffer, open window, and run command +function TermRun(cmd) + local info = TermShow() + + -- Send command to terminal + vim.fn.chansend(info.job_id, '\021' .. cmd .. '\n') + vim.fn.feedkeys("G", "n") +end + +-- Get terminal buffer and job_id if valid, returns {buf, job_id, win} +-- win is nil if terminal is not currently visible +function GetTermInfo() + if Lemon.term_buf == nil or not vim.api.nvim_buf_is_valid(Lemon.term_buf) then + return nil + end + + local job_id = vim.api.nvim_buf_get_var(Lemon.term_buf, 'terminal_job_id') + + -- Find window showing the terminal buffer + local win = nil + for _, w in ipairs(vim.api.nvim_list_wins()) do + if vim.api.nvim_win_get_buf(w) == Lemon.term_buf then + win = w + break + end + end + + return { buf = Lemon.term_buf, job_id = job_id, win = win } +end + +-- Compatibility wrapper - returns window ID if terminal is visible +function SwitchToExistingTerm() + local info = GetTermInfo() + return info and info.win or nil +end + +-- Runs the make command and runs the callback when it completes +function MakeAnd(run_callback) + -- Create a one-time autocmd that fires when make completes + local group = vim.api.nvim_create_augroup('MakeAnd', { clear = false }) + + vim.api.nvim_create_autocmd('QuickFixCmdPost', { + group = group, + pattern = 'make', + once = true, + callback = function() + local qf_list = vim.fn.getqflist() + local has_errors = false + + for _, item in ipairs(qf_list) do + if item.valid == 1 then + has_errors = true + break + end + end + + vim.schedule(function() + if not has_errors then + run_callback() + else + vim.api.nvim_echo({ { "Build failed", "ErrorMsg" } }, false, {}) + end + end) + end + }) + + vim.cmd('silent make') +end + +function TabCurrent() + return vim.fn.tabpagenr() +end + +function TabSwitch(tab) + vim.cmd('tabnext ' .. tab) +end diff --git a/posts/blog-server.md b/posts/blog-server.md index d5e614a..678eb09 100644 --- a/posts/blog-server.md +++ b/posts/blog-server.md @@ -1,10 +1,79 @@ # Blog setup with markdown, rust & git -I've recently went trough a lot of changes in my career such as starting freelancing, setting up my own projects and wanting to take a serious attempt at solo game-dev. With this I figured it'd be nice to have a dev blog of sorts to keep track of progress and maybe share some interesting research or developments. +Hey everyone. This is the first post I write for the blog I'm starting, which is Coincidentally about how I've structured the software surrounding my blog. + +First off, I've been wanting to do a lot recently: + +- Moving away from Windows to using Linux and MacOS for development and gaming +- Switching from VSCode/Cursor to neovim +- Switching from working for an employer to becoming a solo dev/freelancer +- Dedicate time to solo game development + +Because of this, I figured it would be a good time to set up a blog, both for documenting whatever I'm working on and to get the word out about my work. ## The blog design I wanted to have a setup that generates static html from something like markdown. Then recently I stumbled across this [https://gaultier.github.io/blog/making_my_static_blog_generator_11_times_faster.html](https://gaultier.github.io/blog/making_my_static_blog_generator_11_times_faster.html) +I decided to do something similar, however I used rust as it has some popular existing libraries for web servers and parsing markdown. The reason for writing an application to host the blog is that I wanted to have it automatically respond to a git webhook which would automatically pull the latest git repo and then rebuild the articles from their markdown files. + +## HTML + +Of course there is also some styling and markup required for the shell, like the navigation bar and footers.\ +I decided to go with a simple list of html templates that get included by the application and scan it for tags to inject certain magic values, like the list of posts or timestamps. + +For example, here is what the page you're looking at looks like: + +```html +$ +
+ $ +
+

← Back to home

+$ +``` + +Which can be parsed very quickly by a simple scanner that triggers on $< and then reads the key and map of `` parameters. + +This allows me to just write template code like this: +```html +$<title default="Guus' blog" pre="Guus - "> +``` + +and process it in rust like this: +```rs +"title" => { + if let Some(post_name) = current_post { + let post = post_manager + .get_post(post_name) + .await + .ok_or_else(|| format!("Post '{}' not found", post_name))?; + + let none: String = "".to_string(); + let pre = attrs.get("pre").unwrap_or(&none); + let postfix = attrs.get("post").unwrap_or(&none); + + Ok(format!("{}{}{}", pre, post.title, postfix)) + } else { + let def = attrs + .get("default") + .and_then(|s| Some(s.clone())) + .unwrap_or("".to_string()); + Ok(def.to_string()) + } +} +``` + +All the HTML code that has been generated like this is then cached until the next the blog update is triggered by my pushing a new post to git. + +## Styling + +For the styling, I added some minimal CSS to center the contents, limit the width and change the font sizes.\ +I wanted to keep it simple so the layout works on both a desktop browser, and on mobile phone screens. This also allows you to dock the window to a side of your screen or second vertical monitor in case you wanted to reference it for some code which I find useful. + +## Conclusion + +I like this design as it's easy to deploy locally for previewing, and adding new posts or making edits is as simple as `git commit && git push`. I can keep posts I'm working on in separate branches and progressively work on them like that. + +If you're interested in the source code, or would like to use it for yourself feel free to check it out [here](https://git.bakje.coffee/guus/blog). -spall checkaff diff --git a/src/main.rs b/src/main.rs index e8414fa..b21a4b0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,15 +5,24 @@ mod template_engine; use axum::{ extract::{Path, State}, http::StatusCode, - response::{Html, IntoResponse}, + response::{Html, IntoResponse, Response}, routing::get, Router, }; +use clap::Parser; use std::sync::Arc; use tokio::process::Command; use tokio::sync::RwLock; use tower_http::services::ServeDir; +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + /// Disable caching of markdown rendering + #[arg(long)] + no_cache: bool, +} + #[derive(Clone)] struct AppState { post_manager: Arc<RwLock<post_manager::PostManager>>, @@ -21,14 +30,20 @@ struct AppState { #[tokio::main] async fn main() { + let args = Args::parse(); + let post_manager = Arc::new(RwLock::new( - post_manager::PostManager::new(".").expect("Failed to initialize post manager"), + post_manager::PostManager::new(".", args.no_cache).expect("Failed to initialize post manager"), )); let app_state = AppState { post_manager: post_manager.clone(), }; + if args.no_cache { + println!("Running with caching disabled"); + } + let p_router = Router::new() .route("/:post_name", get(post_handler)) .fallback_service(ServeDir::new("posts")); @@ -51,14 +66,14 @@ async fn main() { .expect("Failed to start server"); } -async fn index_handler(State(state): State<AppState>) -> impl IntoResponse { +async fn index_handler(State(state): State<AppState>) -> Response { match render_template("index.html", &state).await { Ok(html) => Html(html).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e).into_response(), } } -async fn all_handler(State(state): State<AppState>, Path(page): Path<String>) -> impl IntoResponse { +async fn all_handler(State(state): State<AppState>, Path(page): Path<String>) -> Response { if page.contains("..") { return (StatusCode::NOT_FOUND, "Invalid path").into_response(); } @@ -71,13 +86,13 @@ async fn all_handler(State(state): State<AppState>, Path(page): Path<String>) -> async fn post_handler( State(state): State<AppState>, Path(post_name): Path<String>, -) -> impl IntoResponse { +) -> Response { if post_name.contains("..") { return (StatusCode::NOT_FOUND, "Invalid path").into_response(); } let manager = state.post_manager.read().await; - match manager.get_post(&post_name) { + match manager.get_post(&post_name).await { Some(_post) => { drop(manager); match render_post_template(&state, post_name).await { @@ -148,7 +163,7 @@ async fn render_template(template_name: &str, state: &AppState) -> Result<String .map_err(|e| format!("Failed to read template: {}", e))?; let manager = state.post_manager.read().await; - template_engine::render_template(&template_content, &*manager, None) + template_engine::render_template(&template_content, &*manager, None).await } async fn render_post_template(state: &AppState, post_name: String) -> Result<String, String> { @@ -156,5 +171,5 @@ async fn render_post_template(state: &AppState, post_name: String) -> Result<Str .map_err(|e| format!("Failed to read post template: {}", e))?; let manager = state.post_manager.read().await; - template_engine::render_template(&template_content, &*manager, Some(&post_name)) + template_engine::render_template(&template_content, &*manager, Some(&post_name)).await } diff --git a/src/post_manager.rs b/src/post_manager.rs index dca7bdb..7a9023f 100644 --- a/src/post_manager.rs +++ b/src/post_manager.rs @@ -1,9 +1,11 @@ use chrono::{DateTime, Utc}; -use pulldown_cmark::{html, Event, HeadingLevel, Options, Parser, Tag}; +use pulldown_cmark::{html, Event, HeadingLevel, Options, Parser, Tag, TagEnd}; use std::collections::HashMap; use std::fs; use std::path::PathBuf; use std::process::Command; +use std::sync::Arc; +use tokio::sync::RwLock; #[derive(Debug, Clone)] pub struct Post { @@ -30,11 +32,12 @@ struct Entry { pub struct PostManager { root_dir: PathBuf, posts_dir_rel: String, - posts: HashMap<String, Post>, + posts: HashMap<String, Arc<RwLock<Post>>>, + no_cache: bool, } impl PostManager { - pub fn new(root_dir: &str) -> Result<Self, String> { + pub fn new(root_dir: &str, no_cache: bool) -> Result<Self, String> { let root_dir = PathBuf::from(root_dir); let posts_dir_rel = "posts"; @@ -42,6 +45,7 @@ impl PostManager { root_dir, posts_dir_rel: posts_dir_rel.to_string(), posts: HashMap::new(), + no_cache, }; manager.refresh_posts()?; @@ -155,37 +159,68 @@ impl PostManager { modified_at: entry.modified_at, }; eprintln!("Loaded post: {} ({})", name, entry.created_at); - self.posts.insert(name, post); + self.posts.insert(name, Arc::new(RwLock::new(post))); } println!("Loaded {} posts", self.posts.len()); Ok(()) } - pub fn get_post(&self, name: &str) -> Option<&Post> { - self.posts.get(name) + pub async fn get_post(&self, name: &str) -> Option<Post> { + let post_lock = self.posts.get(name)?; + + // If no_cache is enabled, always regenerate + if self.no_cache { + self.refresh_post_cache(name, post_lock).await; + } + + let post = post_lock.read().await; + Some(post.clone()) + } + + async fn refresh_post_cache(&self, name: &str, post_lock: &Arc<RwLock<Post>>) { + let mut post = post_lock.write().await; + let filename = post.filename.clone(); + + if let Ok(markdown_content) = fs::read_to_string(&filename) { + let (html_content, title) = markdown_to_html(&markdown_content); + post.html_content = html_content; + post.title = title; + post.markdown_content = markdown_content; + eprintln!("Refreshed post '{}'", name); + } } // Get all posts, sorted by creation date - pub fn get_all_posts(&self) -> Vec<&Post> { - let mut posts: Vec<&Post> = self.posts.values().collect(); + pub async fn get_all_posts(&self) -> Vec<Post> { + let mut posts = Vec::new(); + for (name, post_lock) in &self.posts { + // If no_cache is enabled, always regenerate + if self.no_cache { + self.refresh_post_cache(name, post_lock).await; + } + + let post = post_lock.read().await; + posts.push(post.clone()); + } posts.sort_by(|a, b| b.created_at.cmp(&a.created_at)); posts } // Get the timstamp of when the blog was most recently updated // derived from latest post update - pub fn get_update_timestamp(&self) -> Result<DateTime<Utc>, String> { - let mut posts: Vec<&Post> = self.posts.values().collect(); - posts.sort_by(|a, b| b.modified_at.cmp(&a.modified_at)); - Ok(posts + pub async fn get_update_timestamp(&self) -> Result<DateTime<Utc>, String> { + let posts = self.get_all_posts().await; + let mut posts_sorted: Vec<&Post> = posts.iter().collect(); + posts_sorted.sort_by(|a, b| b.modified_at.cmp(&a.modified_at)); + Ok(posts_sorted .first() .ok_or("No posts found".to_string())? .created_at) } - pub fn get_posts_limited(&self, limit: usize) -> Vec<&Post> { - let mut posts = self.get_all_posts(); + pub async fn get_posts_limited(&self, limit: usize) -> Vec<Post> { + let mut posts = self.get_all_posts().await; posts.truncate(limit); posts } @@ -193,20 +228,20 @@ impl PostManager { fn markdown_title(markdown: &str) -> Option<String> { let parser = Parser::new(markdown); + let mut in_tag = false; for event in parser { - if let Event::Start(tag) = event { - if let Tag::Heading { - level, - id, + match event { + Event::Start(Tag::Heading { + level: HeadingLevel::H1, .. - } = tag - { - if level == HeadingLevel::H1 { - if let Some(str) = id { - return Some(str.to_string()); - } + }) => in_tag = true, + Event::End(TagEnd::Heading(HeadingLevel::H1)) => in_tag = false, + Event::Text(txt) => { + if in_tag { + return Some(txt.to_string()); } } + _ => {}, } } None diff --git a/src/template_engine.rs b/src/template_engine.rs index 3fa4ca8..f88801d 100644 --- a/src/template_engine.rs +++ b/src/template_engine.rs @@ -1,17 +1,28 @@ -use crate::post_manager::PostManager; use crate::git_tracker::get_git_version; +use crate::post_manager::PostManager; use std::collections::HashMap; use std::fs; +use std::future::Future; +use std::pin::Pin; -pub fn render_template( +pub async fn render_template( template: &str, post_manager: &PostManager, current_post: Option<&str>, ) -> Result<String, String> { - parse_template(template, post_manager, current_post, 0) + parse_template(template, post_manager, current_post, 0).await } -fn parse_template( +fn parse_template<'a>( + template: &'a str, + post_manager: &'a PostManager, + current_post: Option<&'a str>, + depth: usize, +) -> Pin<Box<dyn Future<Output = Result<String, String>> + Send + 'a>> { + Box::pin(async move { parse_template_impl(template, post_manager, current_post, depth).await }) +} + +async fn parse_template_impl( template: &str, post_manager: &PostManager, current_post: Option<&str>, @@ -32,7 +43,8 @@ fn parse_template( // Try to parse tag if let Some((tag_name, attrs, end_pos)) = parse_tag(&chars, i) { // Process tag immediately based on type - let content = process_tag(&tag_name, &attrs, post_manager, current_post, depth)?; + let content = + process_tag(&tag_name, &attrs, post_manager, current_post, depth).await?; result.push_str(&content); i = end_pos; continue; @@ -140,7 +152,7 @@ fn parse_tag(chars: &[char], start: usize) -> Option<(String, HashMap<String, St } } -fn process_tag( +async fn process_tag( tag_name: &str, attrs: &HashMap<String, String>, post_manager: &PostManager, @@ -149,7 +161,8 @@ fn process_tag( ) -> Result<String, String> { match tag_name { "include" => { - let src = attrs.get("src") + let src = attrs + .get("src") .ok_or_else(|| "include tag missing 'src' attribute".to_string())?; let include_path = format!("templates/{}", src); @@ -157,7 +170,7 @@ fn process_tag( .map_err(|e| format!("Failed to read include file '{}': {}", src, e))?; // Recursively parse the included content - parse_template(&content, post_manager, current_post, depth + 1) + parse_template(&content, post_manager, current_post, depth + 1).await } "post-html" => { @@ -166,28 +179,43 @@ fn process_tag( let post = post_manager .get_post(post_name) + .await .ok_or_else(|| format!("Post '{}' not found", post_name))?; Ok(post.html_content.clone()) } "title" => { - let post_name = current_post - .ok_or_else(|| "title tag used outside of post context".to_string())?; + if let Some(post_name) = current_post { + let post = post_manager + .get_post(post_name) + .await + .ok_or_else(|| format!("Post '{}' not found", post_name))?; - let post = post_manager - .get_post(post_name) - .ok_or_else(|| format!("Post '{}' not found", post_name))?; + let none: String = "".to_string(); + let pre = attrs.get("pre").unwrap_or(&none); + let postfix = attrs.get("post").unwrap_or(&none); - Ok(post.title.to_string()) + Ok(format!("{}{}{}", pre, post.title, postfix)) + } else { + let def = attrs + .get("default") + .and_then(|s| Some(s.clone())) + .unwrap_or("<title not set>".to_string()); + Ok(def.to_string()) + } } "updated" => { // Insert the last updated time of the current post (or most recent post) let post_ts = if let Some(p) = current_post { - post_manager.get_post(p).ok_or_else(|| format!("Post '{}' not found", p))?.modified_at + post_manager + .get_post(p) + .await + .ok_or_else(|| format!("Post '{}' not found", p))? + .modified_at } else { - post_manager.get_update_timestamp()? + post_manager.get_update_timestamp().await? }; Ok(post_ts.format("%Y-%m-%d").to_string()) @@ -200,13 +228,12 @@ fn process_tag( } "post-list" => { - let limit = attrs.get("limit") - .and_then(|s| s.parse::<usize>().ok()); + let limit = attrs.get("limit").and_then(|s| s.parse::<usize>().ok()); let posts = if let Some(l) = limit { - post_manager.get_posts_limited(l) + post_manager.get_posts_limited(l).await } else { - post_manager.get_all_posts() + post_manager.get_all_posts().await }; let mut list_html = String::from("<ul class=\"post-list\">\n"); @@ -214,7 +241,7 @@ fn process_tag( list_html.push_str(&format!( " <li><a href=\"/p/{}\">{}</a> <span class=\"date\">{}</span></li>\n", post.name, - post.name, + post.title, post.created_at.format("%Y-%m-%d") )); } @@ -223,7 +250,7 @@ fn process_tag( Ok(list_html) } - _ => Err(format!("Unknown tag: {}", tag_name)) + _ => Err(format!("Unknown tag: {}", tag_name)), } } diff --git a/templates/header.html b/templates/header.html index 66980ce..cbfc3d8 100644 --- a/templates/header.html +++ b/templates/header.html @@ -3,7 +3,7 @@ <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>My Blog + $<title default="Guus' blog" pre="Guus - "> @@ -95,7 +96,12 @@ (dis) gus' things Home All Posts - + +