Compare commits
3 Commits
565342e62f
...
c584d39607
| Author | SHA1 | Date | |
|---|---|---|---|
| c584d39607 | |||
| 185d9d4d63 | |||
| 9d5f86ad46 |
127
Cargo.lock
generated
127
Cargo.lock
generated
@@ -11,6 +11,56 @@ dependencies = [
|
|||||||
"libc",
|
"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]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.89"
|
version = "0.1.89"
|
||||||
@@ -101,6 +151,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"clap",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"pulldown-cmark",
|
"pulldown-cmark",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -150,6 +201,52 @@ dependencies = [
|
|||||||
"windows-link",
|
"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]]
|
[[package]]
|
||||||
name = "core-foundation-sys"
|
name = "core-foundation-sys"
|
||||||
version = "0.8.7"
|
version = "0.8.7"
|
||||||
@@ -225,6 +322,12 @@ dependencies = [
|
|||||||
"unicode-width",
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
@@ -338,6 +441,12 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is_terminal_polyfill"
|
||||||
|
version = "1.70.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.15"
|
version = "1.0.15"
|
||||||
@@ -429,6 +538,12 @@ version = "1.21.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell_polyfill"
|
||||||
|
version = "1.70.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.5"
|
version = "0.12.5"
|
||||||
@@ -631,6 +746,12 @@ dependencies = [
|
|||||||
"windows-sys 0.60.2",
|
"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]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.108"
|
version = "2.0.108"
|
||||||
@@ -791,6 +912,12 @@ version = "0.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8parse"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.11.1+wasi-snapshot-preview1"
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
|
|||||||
@@ -12,3 +12,4 @@ pulldown-cmark = "0.12"
|
|||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
once_cell = "1.19"
|
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
|
||||||
79
posts/blog-server.md
Normal file
79
posts/blog-server.md
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# Blog setup with markdown, rust & git
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
-- Load persistent configuration from .workspace.lua
|
require("lua/lemon")
|
||||||
local ws_file = "./.nvim.workspace.lua"
|
|
||||||
local loaded, workspace = pcall(dofile, ws_file)
|
local def_workspace = { args = {}, build_type = "debug", binary = "blog-server" }
|
||||||
if not loaded then
|
local workspace = InitWorkspace(def_workspace)
|
||||||
workspace = { args = {}, build_type = "debug", binary = "blog-server" }
|
|
||||||
end
|
|
||||||
|
|
||||||
local build_folder
|
local build_folder
|
||||||
local bin_target
|
local bin_target
|
||||||
@@ -47,35 +45,6 @@ vim.api.nvim_create_autocmd("FileType", {
|
|||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
local 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(workspace); nl()
|
|
||||||
vim.fn.writefile(s.ls, ws_file)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- nvim-dap configuration
|
-- nvim-dap configuration
|
||||||
local dap_ok, dap = pcall(require, "dap")
|
local dap_ok, dap = pcall(require, "dap")
|
||||||
local dap_def_cfg
|
local dap_def_cfg
|
||||||
@@ -85,22 +54,7 @@ local dap_configs
|
|||||||
local function updateArgs(args)
|
local function updateArgs(args)
|
||||||
workspace.args = args
|
workspace.args = args
|
||||||
if dap_configs ~= nil then dap_configs[1].args = args end
|
if dap_configs ~= nil then dap_configs[1].args = args end
|
||||||
writeWorkspace()
|
WriteWorkspace()
|
||||||
end
|
|
||||||
|
|
||||||
-- Find terminal tab or create new one, then run command
|
|
||||||
local function TermRun(cmd)
|
|
||||||
local found = false
|
|
||||||
for i = 1, vim.fn.tabpagenr('$') do
|
|
||||||
vim.cmd('tabnext ' .. i)
|
|
||||||
if vim.bo.buftype == 'terminal' then
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if not found then vim.cmd('tabnew | terminal') end
|
|
||||||
vim.fn.chansend(vim.b.terminal_job_id, '' .. cmd .. '\n')
|
|
||||||
vim.fn.feedkeys("G", "n")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- The Configure command
|
-- The Configure command
|
||||||
@@ -110,7 +64,7 @@ vim.api.nvim_create_user_command("Configure", function(a)
|
|||||||
if #a.args > 0 then bt = a.args end
|
if #a.args > 0 then bt = a.args end
|
||||||
workspace.build_type = bt
|
workspace.build_type = bt
|
||||||
updateBuildEnv()
|
updateBuildEnv()
|
||||||
writeWorkspace()
|
WriteWorkspace()
|
||||||
end, { nargs = '?', desc = "Update run/debug arguments" })
|
end, { nargs = '?', desc = "Update run/debug arguments" })
|
||||||
|
|
||||||
vim.api.nvim_create_user_command("Args", function(a) updateArgs(a.fargs) end,
|
vim.api.nvim_create_user_command("Args", function(a) updateArgs(a.fargs) end,
|
||||||
@@ -125,7 +79,7 @@ if dap_ok then
|
|||||||
}
|
}
|
||||||
dap_configs = {
|
dap_configs = {
|
||||||
{
|
{
|
||||||
name = 'test all',
|
name = 'default',
|
||||||
type = 'codelldb',
|
type = 'codelldb',
|
||||||
request = 'launch',
|
request = 'launch',
|
||||||
program = bin_name,
|
program = bin_name,
|
||||||
@@ -147,7 +101,6 @@ if dap_ok then
|
|||||||
end, { nargs = '*', desc = "Starts debugging with specified arguments" })
|
end, { nargs = '*', desc = "Starts debugging with specified arguments" })
|
||||||
end
|
end
|
||||||
|
|
||||||
if MakeAnd then
|
|
||||||
local r = function()
|
local r = function()
|
||||||
MakeAnd(function()
|
MakeAnd(function()
|
||||||
TermRun(bin_name .. " " .. table.concat(workspace.args, " "))
|
TermRun(bin_name .. " " .. table.concat(workspace.args, " "))
|
||||||
@@ -161,6 +114,17 @@ if MakeAnd then
|
|||||||
|
|
||||||
-- F6 to run the application
|
-- F6 to run the application
|
||||||
vim.keymap.set('n', '<F6>', r)
|
vim.keymap.set('n', '<F6>', r)
|
||||||
|
vim.keymap.set('n', '<F18>', function()
|
||||||
|
local info = GetTermInfo()
|
||||||
|
if info then
|
||||||
|
-- Send interrupt to terminal without switching
|
||||||
|
vim.fn.chansend(info.job_id, '\003')
|
||||||
|
-- Close window if it's open
|
||||||
|
vim.print("Stopped program")
|
||||||
|
else
|
||||||
|
vim.print("No terminal buffer found")
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
if dap_ok then
|
if dap_ok then
|
||||||
-- Shift-F5 to launch default config
|
-- Shift-F5 to launch default config
|
||||||
@@ -170,4 +134,3 @@ if MakeAnd then
|
|||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
31
src/main.rs
31
src/main.rs
@@ -5,15 +5,24 @@ mod template_engine;
|
|||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
response::{Html, IntoResponse},
|
response::{Html, IntoResponse, Response},
|
||||||
routing::get,
|
routing::get,
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
|
use clap::Parser;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use tower_http::services::ServeDir;
|
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)]
|
#[derive(Clone)]
|
||||||
struct AppState {
|
struct AppState {
|
||||||
post_manager: Arc<RwLock<post_manager::PostManager>>,
|
post_manager: Arc<RwLock<post_manager::PostManager>>,
|
||||||
@@ -21,14 +30,20 @@ struct AppState {
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
let post_manager = Arc::new(RwLock::new(
|
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 {
|
let app_state = AppState {
|
||||||
post_manager: post_manager.clone(),
|
post_manager: post_manager.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if args.no_cache {
|
||||||
|
println!("Running with caching disabled");
|
||||||
|
}
|
||||||
|
|
||||||
let p_router = Router::new()
|
let p_router = Router::new()
|
||||||
.route("/:post_name", get(post_handler))
|
.route("/:post_name", get(post_handler))
|
||||||
.fallback_service(ServeDir::new("posts"));
|
.fallback_service(ServeDir::new("posts"));
|
||||||
@@ -51,14 +66,14 @@ async fn main() {
|
|||||||
.expect("Failed to start server");
|
.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 {
|
match render_template("index.html", &state).await {
|
||||||
Ok(html) => Html(html).into_response(),
|
Ok(html) => Html(html).into_response(),
|
||||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e).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("..") {
|
if page.contains("..") {
|
||||||
return (StatusCode::NOT_FOUND, "Invalid path").into_response();
|
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(
|
async fn post_handler(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(post_name): Path<String>,
|
Path(post_name): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> Response {
|
||||||
if post_name.contains("..") {
|
if post_name.contains("..") {
|
||||||
return (StatusCode::NOT_FOUND, "Invalid path").into_response();
|
return (StatusCode::NOT_FOUND, "Invalid path").into_response();
|
||||||
}
|
}
|
||||||
|
|
||||||
let manager = state.post_manager.read().await;
|
let manager = state.post_manager.read().await;
|
||||||
match manager.get_post(&post_name) {
|
match manager.get_post(&post_name).await {
|
||||||
Some(_post) => {
|
Some(_post) => {
|
||||||
drop(manager);
|
drop(manager);
|
||||||
match render_post_template(&state, post_name).await {
|
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))?;
|
.map_err(|e| format!("Failed to read template: {}", e))?;
|
||||||
|
|
||||||
let manager = state.post_manager.read().await;
|
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> {
|
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))?;
|
.map_err(|e| format!("Failed to read post template: {}", e))?;
|
||||||
|
|
||||||
let manager = state.post_manager.read().await;
|
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,13 +1,16 @@
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use pulldown_cmark::{html, Options, Parser};
|
use pulldown_cmark::{html, Event, HeadingLevel, Options, Parser, Tag, TagEnd};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Post {
|
pub struct Post {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
pub title: String,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub filename: String,
|
pub filename: String,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@@ -29,11 +32,12 @@ struct Entry {
|
|||||||
pub struct PostManager {
|
pub struct PostManager {
|
||||||
root_dir: PathBuf,
|
root_dir: PathBuf,
|
||||||
posts_dir_rel: String,
|
posts_dir_rel: String,
|
||||||
posts: HashMap<String, Post>,
|
posts: HashMap<String, Arc<RwLock<Post>>>,
|
||||||
|
no_cache: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PostManager {
|
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 root_dir = PathBuf::from(root_dir);
|
||||||
let posts_dir_rel = "posts";
|
let posts_dir_rel = "posts";
|
||||||
|
|
||||||
@@ -41,6 +45,7 @@ impl PostManager {
|
|||||||
root_dir,
|
root_dir,
|
||||||
posts_dir_rel: posts_dir_rel.to_string(),
|
posts_dir_rel: posts_dir_rel.to_string(),
|
||||||
posts: HashMap::new(),
|
posts: HashMap::new(),
|
||||||
|
no_cache,
|
||||||
};
|
};
|
||||||
|
|
||||||
manager.refresh_posts()?;
|
manager.refresh_posts()?;
|
||||||
@@ -143,9 +148,10 @@ impl PostManager {
|
|||||||
}
|
}
|
||||||
let markdown_content = fs::read_to_string(&entry.file_path)
|
let markdown_content = fs::read_to_string(&entry.file_path)
|
||||||
.map_err(|e| format!("Failed to read post file: {}", e))?;
|
.map_err(|e| format!("Failed to read post file: {}", e))?;
|
||||||
let html_content = markdown_to_html(&markdown_content);
|
let (html_content, title) = markdown_to_html(&markdown_content);
|
||||||
let post = Post {
|
let post = Post {
|
||||||
name: name.clone(),
|
name: name.clone(),
|
||||||
|
title: title,
|
||||||
filename: entry.file_path,
|
filename: entry.file_path,
|
||||||
markdown_content,
|
markdown_content,
|
||||||
html_content,
|
html_content,
|
||||||
@@ -153,43 +159,95 @@ impl PostManager {
|
|||||||
modified_at: entry.modified_at,
|
modified_at: entry.modified_at,
|
||||||
};
|
};
|
||||||
eprintln!("Loaded post: {} ({})", name, entry.created_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());
|
println!("Loaded {} posts", self.posts.len());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_post(&self, name: &str) -> Option<&Post> {
|
pub async fn get_post(&self, name: &str) -> Option<Post> {
|
||||||
self.posts.get(name)
|
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
|
// Get all posts, sorted by creation date
|
||||||
pub fn get_all_posts(&self) -> Vec<&Post> {
|
pub async fn get_all_posts(&self) -> Vec<Post> {
|
||||||
let mut posts: Vec<&Post> = self.posts.values().collect();
|
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.sort_by(|a, b| b.created_at.cmp(&a.created_at));
|
||||||
posts
|
posts
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the timstamp of when the blog was most recently updated
|
// Get the timstamp of when the blog was most recently updated
|
||||||
// derived from latest post update
|
// derived from latest post update
|
||||||
pub fn get_update_timestamp(&self) -> Result<DateTime<Utc>, String> {
|
pub async fn get_update_timestamp(&self) -> Result<DateTime<Utc>, String> {
|
||||||
let mut posts: Vec<&Post> = self.posts.values().collect();
|
let posts = self.get_all_posts().await;
|
||||||
posts.sort_by(|a, b| b.modified_at.cmp(&a.modified_at));
|
let mut posts_sorted: Vec<&Post> = posts.iter().collect();
|
||||||
Ok(posts
|
posts_sorted.sort_by(|a, b| b.modified_at.cmp(&a.modified_at));
|
||||||
|
Ok(posts_sorted
|
||||||
.first()
|
.first()
|
||||||
.ok_or("No posts found".to_string())?
|
.ok_or("No posts found".to_string())?
|
||||||
.created_at)
|
.created_at)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_posts_limited(&self, limit: usize) -> Vec<&Post> {
|
pub async fn get_posts_limited(&self, limit: usize) -> Vec<Post> {
|
||||||
let mut posts = self.get_all_posts();
|
let mut posts = self.get_all_posts().await;
|
||||||
posts.truncate(limit);
|
posts.truncate(limit);
|
||||||
posts
|
posts
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn markdown_to_html(markdown: &str) -> String {
|
fn markdown_title(markdown: &str) -> Option<String> {
|
||||||
|
let parser = Parser::new(markdown);
|
||||||
|
let mut in_tag = false;
|
||||||
|
for event in parser {
|
||||||
|
match event {
|
||||||
|
Event::Start(Tag::Heading {
|
||||||
|
level: HeadingLevel::H1,
|
||||||
|
..
|
||||||
|
}) => in_tag = true,
|
||||||
|
Event::End(TagEnd::Heading(HeadingLevel::H1)) => in_tag = false,
|
||||||
|
Event::Text(txt) => {
|
||||||
|
if in_tag {
|
||||||
|
return Some(txt.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn markdown_to_html(markdown: &str) -> (String, String) {
|
||||||
let mut options = Options::empty();
|
let mut options = Options::empty();
|
||||||
options.insert(Options::ENABLE_STRIKETHROUGH);
|
options.insert(Options::ENABLE_STRIKETHROUGH);
|
||||||
options.insert(Options::ENABLE_TABLES);
|
options.insert(Options::ENABLE_TABLES);
|
||||||
@@ -197,8 +255,10 @@ fn markdown_to_html(markdown: &str) -> String {
|
|||||||
options.insert(Options::ENABLE_TASKLISTS);
|
options.insert(Options::ENABLE_TASKLISTS);
|
||||||
options.insert(Options::ENABLE_SMART_PUNCTUATION);
|
options.insert(Options::ENABLE_SMART_PUNCTUATION);
|
||||||
|
|
||||||
|
let title = markdown_title(markdown).unwrap_or("unknown".to_string());
|
||||||
|
|
||||||
let parser = Parser::new_ext(markdown, options);
|
let parser = Parser::new_ext(markdown, options);
|
||||||
let mut html_output = String::new();
|
let mut html_output = String::new();
|
||||||
html::push_html(&mut html_output, parser);
|
html::push_html(&mut html_output, parser);
|
||||||
html_output
|
(html_output, title)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,28 @@
|
|||||||
use crate::post_manager::PostManager;
|
|
||||||
use crate::git_tracker::get_git_version;
|
use crate::git_tracker::get_git_version;
|
||||||
|
use crate::post_manager::PostManager;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::future::Future;
|
||||||
|
use std::pin::Pin;
|
||||||
|
|
||||||
pub fn render_template(
|
pub async fn render_template(
|
||||||
template: &str,
|
template: &str,
|
||||||
post_manager: &PostManager,
|
post_manager: &PostManager,
|
||||||
current_post: Option<&str>,
|
current_post: Option<&str>,
|
||||||
) -> Result<String, String> {
|
) -> 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,
|
template: &str,
|
||||||
post_manager: &PostManager,
|
post_manager: &PostManager,
|
||||||
current_post: Option<&str>,
|
current_post: Option<&str>,
|
||||||
@@ -32,7 +43,8 @@ fn parse_template(
|
|||||||
// Try to parse tag
|
// Try to parse tag
|
||||||
if let Some((tag_name, attrs, end_pos)) = parse_tag(&chars, i) {
|
if let Some((tag_name, attrs, end_pos)) = parse_tag(&chars, i) {
|
||||||
// Process tag immediately based on type
|
// 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);
|
result.push_str(&content);
|
||||||
i = end_pos;
|
i = end_pos;
|
||||||
continue;
|
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,
|
tag_name: &str,
|
||||||
attrs: &HashMap<String, String>,
|
attrs: &HashMap<String, String>,
|
||||||
post_manager: &PostManager,
|
post_manager: &PostManager,
|
||||||
@@ -149,7 +161,8 @@ fn process_tag(
|
|||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
match tag_name {
|
match tag_name {
|
||||||
"include" => {
|
"include" => {
|
||||||
let src = attrs.get("src")
|
let src = attrs
|
||||||
|
.get("src")
|
||||||
.ok_or_else(|| "include tag missing 'src' attribute".to_string())?;
|
.ok_or_else(|| "include tag missing 'src' attribute".to_string())?;
|
||||||
|
|
||||||
let include_path = format!("templates/{}", src);
|
let include_path = format!("templates/{}", src);
|
||||||
@@ -157,7 +170,7 @@ fn process_tag(
|
|||||||
.map_err(|e| format!("Failed to read include file '{}': {}", src, e))?;
|
.map_err(|e| format!("Failed to read include file '{}': {}", src, e))?;
|
||||||
|
|
||||||
// Recursively parse the included content
|
// 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" => {
|
"post-html" => {
|
||||||
@@ -166,17 +179,43 @@ fn process_tag(
|
|||||||
|
|
||||||
let post = post_manager
|
let post = post_manager
|
||||||
.get_post(post_name)
|
.get_post(post_name)
|
||||||
|
.await
|
||||||
.ok_or_else(|| format!("Post '{}' not found", post_name))?;
|
.ok_or_else(|| format!("Post '{}' not found", post_name))?;
|
||||||
|
|
||||||
Ok(post.html_content.clone())
|
Ok(post.html_content.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
"updated" => {
|
"updated" => {
|
||||||
// Insert the last updated time of the current post (or most recent post)
|
// Insert the last updated time of the current post (or most recent post)
|
||||||
let post_ts = if let Some(p) = current_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 {
|
} else {
|
||||||
post_manager.get_update_timestamp()?
|
post_manager.get_update_timestamp().await?
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(post_ts.format("%Y-%m-%d").to_string())
|
Ok(post_ts.format("%Y-%m-%d").to_string())
|
||||||
@@ -189,13 +228,12 @@ fn process_tag(
|
|||||||
}
|
}
|
||||||
|
|
||||||
"post-list" => {
|
"post-list" => {
|
||||||
let limit = attrs.get("limit")
|
let limit = attrs.get("limit").and_then(|s| s.parse::<usize>().ok());
|
||||||
.and_then(|s| s.parse::<usize>().ok());
|
|
||||||
|
|
||||||
let posts = if let Some(l) = limit {
|
let posts = if let Some(l) = limit {
|
||||||
post_manager.get_posts_limited(l)
|
post_manager.get_posts_limited(l).await
|
||||||
} else {
|
} else {
|
||||||
post_manager.get_all_posts()
|
post_manager.get_all_posts().await
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut list_html = String::from("<ul class=\"post-list\">\n");
|
let mut list_html = String::from("<ul class=\"post-list\">\n");
|
||||||
@@ -203,7 +241,7 @@ fn process_tag(
|
|||||||
list_html.push_str(&format!(
|
list_html.push_str(&format!(
|
||||||
" <li><a href=\"/p/{}\">{}</a> <span class=\"date\">{}</span></li>\n",
|
" <li><a href=\"/p/{}\">{}</a> <span class=\"date\">{}</span></li>\n",
|
||||||
post.name,
|
post.name,
|
||||||
post.name,
|
post.title,
|
||||||
post.created_at.format("%Y-%m-%d")
|
post.created_at.format("%Y-%m-%d")
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -212,7 +250,7 @@ fn process_tag(
|
|||||||
Ok(list_html)
|
Ok(list_html)
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => Err(format!("Unknown tag: {}", tag_name))
|
_ => Err(format!("Unknown tag: {}", tag_name)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>My Blog</title>
|
<title>$<title default="Guus' blog" pre="Guus - "></title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: system-ui, -apple-system, sans-serif;
|
font-family: system-ui, -apple-system, sans-serif;
|
||||||
@@ -74,18 +74,19 @@
|
|||||||
pre code {
|
pre code {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
.github-link {
|
.social-link {
|
||||||
float: right;
|
float: right;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
margin-left: 0px;
|
||||||
}
|
}
|
||||||
.github-link svg {
|
.social-link svg {
|
||||||
fill: #333;
|
fill: #333;
|
||||||
transition: fill 0.2s;
|
transition: fill 0.2s;
|
||||||
}
|
}
|
||||||
.github-link:hover svg {
|
.social-link:hover svg {
|
||||||
fill: #0066cc;
|
fill: #0066cc;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -95,7 +96,12 @@
|
|||||||
<span>(dis) gus' things</span>
|
<span>(dis) gus' things</span>
|
||||||
<a href="/">Home</a>
|
<a href="/">Home</a>
|
||||||
<a href="/all">All Posts</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">
|
<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"/>
|
<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>
|
</svg>
|
||||||
|
|||||||
Reference in New Issue
Block a user