Update server post and fix post titles
This commit is contained in:
127
Cargo.lock
generated
127
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
152
lua/lemon.lua
Normal file
152
lua/lemon.lua
Normal file
@@ -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
|
||||
@@ -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
|
||||
$<include src="header.html"/>
|
||||
<article>
|
||||
$<post-html/>
|
||||
</article>
|
||||
<p><a href="/">← Back to home</a></p>
|
||||
$<include src="footer.html"/>
|
||||
```
|
||||
|
||||
Which can be parsed very quickly by a simple scanner that triggers on $< and then reads the key and map of `<String,String>` parameters.
|
||||
|
||||
This allows me to just write template code like this:
|
||||
```html
|
||||
<title>$<title default="Guus' blog" pre="Guus - "></title>
|
||||
```
|
||||
|
||||
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("<title not set>".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
|
||||
|
||||
31
src/main.rs
31
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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))?;
|
||||
|
||||
Ok(post.title.to_string())
|
||||
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("<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)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>My Blog</title>
|
||||
<title>$<title default="Guus' blog" pre="Guus - "></title>
|
||||
<style>
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
@@ -74,18 +74,19 @@
|
||||
pre code {
|
||||
padding: 0;
|
||||
}
|
||||
.github-link {
|
||||
.social-link {
|
||||
float: right;
|
||||
display: inline-block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
vertical-align: middle;
|
||||
margin-left: 0px;
|
||||
}
|
||||
.github-link svg {
|
||||
.social-link svg {
|
||||
fill: #333;
|
||||
transition: fill 0.2s;
|
||||
}
|
||||
.github-link:hover svg {
|
||||
.social-link:hover svg {
|
||||
fill: #0066cc;
|
||||
}
|
||||
</style>
|
||||
@@ -95,7 +96,12 @@
|
||||
<span>(dis) gus' things</span>
|
||||
<a href="/">Home</a>
|
||||
<a href="/all">All Posts</a>
|
||||
<a href="https://github.com/guusw" class="github-link" target="_blank" aria-label="View source on Git">
|
||||
<a href="https://bsky.app/profile/guusww.bsky.social" class="social-link" target="_blank" aria-label="Follow on Bluesky">
|
||||
<svg viewBox="0 0 568 501" width="24" height="24" aria-hidden="true">
|
||||
<path d="M123.121 33.664C188.241 82.553 258.281 181.68 284 234.873c25.719-53.192 95.759-152.32 160.879-201.21C491.866-1.611 568-28.906 568 57.947c0 17.346-9.945 145.713-15.778 166.555-20.275 72.453-94.155 90.933-159.875 79.748C507.222 323.8 536.444 388.56 473.333 453.32c-119.86 122.992-172.272-30.859-185.702-70.281-2.462-7.227-3.614-10.608-3.631-7.733-.017-2.875-1.169.506-3.631 7.733-13.43 39.422-65.842 193.273-185.702 70.281-63.111-64.76-33.89-129.52 80.986-149.071-65.72 11.185-139.6-7.295-159.875-79.748C9.945 203.659 0 75.291 0 57.946 0-28.906 76.135-1.612 123.121 33.664Z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://github.com/guusw" class="social-link" target="_blank" aria-label="View source on GitHub">
|
||||
<svg viewBox="0 0 16 16" width="24" height="24" aria-hidden="true">
|
||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
|
||||
</svg>
|
||||
|
||||
Reference in New Issue
Block a user