Compare commits
10 Commits
167ca8c811
...
edc04049d8
| Author | SHA1 | Date | |
|---|---|---|---|
| edc04049d8 | |||
| 40396e0441 | |||
| fb9c58f4ae | |||
| a2edf22954 | |||
| 5928f7d13c | |||
| 7db680831d | |||
| b7221cf343 | |||
| 190667e5a5 | |||
| c0ac63b73c | |||
| c11169701f |
39
Cargo.lock
generated
39
Cargo.lock
generated
@@ -2,15 +2,6 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "aho-corasick"
|
|
||||||
version = "1.1.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "android_system_properties"
|
name = "android_system_properties"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@@ -112,7 +103,6 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"pulldown-cmark",
|
"pulldown-cmark",
|
||||||
"regex",
|
|
||||||
"serde",
|
"serde",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower 0.4.13",
|
"tower 0.4.13",
|
||||||
@@ -526,35 +516,6 @@ dependencies = [
|
|||||||
"bitflags",
|
"bitflags",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "regex"
|
|
||||||
version = "1.12.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
|
|
||||||
dependencies = [
|
|
||||||
"aho-corasick",
|
|
||||||
"memchr",
|
|
||||||
"regex-automata",
|
|
||||||
"regex-syntax",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "regex-automata"
|
|
||||||
version = "0.4.13"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
|
|
||||||
dependencies = [
|
|
||||||
"aho-corasick",
|
|
||||||
"memchr",
|
|
||||||
"regex-syntax",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "regex-syntax"
|
|
||||||
version = "0.8.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.22"
|
version = "1.0.22"
|
||||||
|
|||||||
@@ -11,5 +11,4 @@ tower-http = { version = "0.5", features = ["fs"] }
|
|||||||
pulldown-cmark = "0.12"
|
pulldown-cmark = "0.12"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
regex = "1.10"
|
|
||||||
once_cell = "1.19"
|
once_cell = "1.19"
|
||||||
|
|||||||
18
posts/a-new-post.md
Normal file
18
posts/a-new-post.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# A New Post
|
||||||
|
|
||||||
|
This is a new post.
|
||||||
|
|
||||||
|
## Sub-heading
|
||||||
|
|
||||||
|
This is a sub-heading.
|
||||||
|
|
||||||
|
## Image
|
||||||
|
|
||||||
|
<img src="./a-new-post/photo-1608848461950-0fe51dfc41cb.jpg" alt="drawing" width="200"/>
|
||||||
|
<img src="./a-new-post/photo-1608848461950-0fe51dfc41cb.jpg" alt="drawing" width="200"/>
|
||||||
|
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
I added another **cat** here !!
|
||||||
|
|
||||||
BIN
posts/a-new-post/photo-1608848461950-0fe51dfc41cb.jpg
Normal file
BIN
posts/a-new-post/photo-1608848461950-0fe51dfc41cb.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 246 KiB |
1
posts/hello-world/test.txt
Normal file
1
posts/hello-world/test.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
This is a test file for hello-world post
|
||||||
@@ -117,7 +117,12 @@ vim.api.nvim_create_user_command("Args", function(a) updateArgs(a.fargs) end,
|
|||||||
{ nargs = '*', desc = "Update run/debug arguments" })
|
{ nargs = '*', desc = "Update run/debug arguments" })
|
||||||
|
|
||||||
if dap_ok then
|
if dap_ok then
|
||||||
local lldb_init = {}
|
local rust_path = vim.fn.system("rustc --print sysroot") -- We need to query this to get the sysroot
|
||||||
|
rust_path = string.sub(rust_path, 1, #rust_path - 1) -- trim trailing newline
|
||||||
|
local lldb_init = {
|
||||||
|
"command script import " .. rust_path .. "/lib/rustlib/etc/lldb_lookup.py",
|
||||||
|
"command source " .. rust_path .. "/lib/rustlib/etc/lldb_commands",
|
||||||
|
}
|
||||||
dap_configs = {
|
dap_configs = {
|
||||||
{
|
{
|
||||||
name = 'test all',
|
name = 'test all',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct GitVersion {
|
pub struct GitVersion {
|
||||||
@@ -7,14 +7,7 @@ pub struct GitVersion {
|
|||||||
pub commit: String,
|
pub commit: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GitVersion {
|
pub fn get_git_version(git_path: &PathBuf) -> Result<GitVersion, String> {
|
||||||
pub fn key(&self) -> String {
|
|
||||||
format!("{}/{}", self.branch, self.commit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_git_version(git_dir: &str) -> Result<GitVersion, String> {
|
|
||||||
let git_path = Path::new(git_dir);
|
|
||||||
let head_path = git_path.join(".git/HEAD");
|
let head_path = git_path.join(".git/HEAD");
|
||||||
|
|
||||||
// Read HEAD file
|
// Read HEAD file
|
||||||
|
|||||||
83
src/main.rs
83
src/main.rs
@@ -10,7 +10,9 @@ use axum::{
|
|||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use tokio::process::Command;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
use tower_http::services::ServeDir;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct AppState {
|
struct AppState {
|
||||||
@@ -27,22 +29,15 @@ async fn main() {
|
|||||||
post_manager: post_manager.clone(),
|
post_manager: post_manager.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Spawn background task to watch for git changes
|
let p_router = Router::new()
|
||||||
tokio::spawn(async move {
|
.route("/:post_name", get(post_handler))
|
||||||
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(5));
|
.fallback_service(ServeDir::new("posts"));
|
||||||
loop {
|
|
||||||
interval.tick().await;
|
|
||||||
let mut manager = post_manager.write().await;
|
|
||||||
if let Err(e) = manager.refresh_if_needed() {
|
|
||||||
eprintln!("Error refreshing posts: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/", get(index_handler))
|
.route("/", get(index_handler))
|
||||||
|
.route("/update", get(update_handler))
|
||||||
.route("/:page", get(all_handler))
|
.route("/:page", get(all_handler))
|
||||||
.route("/p/:post_name", get(post_handler))
|
.nest("/p", p_router)
|
||||||
.with_state(app_state);
|
.with_state(app_state);
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
|
||||||
@@ -77,11 +72,11 @@ async fn post_handler(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(post_name): Path<String>,
|
Path(post_name): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let manager = state.post_manager.read().await;
|
if post_name.contains("..") {
|
||||||
|
|
||||||
if post_name.contains("..") || 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;
|
||||||
match manager.get_post(&post_name) {
|
match manager.get_post(&post_name) {
|
||||||
Some(_post) => {
|
Some(_post) => {
|
||||||
drop(manager);
|
drop(manager);
|
||||||
@@ -94,10 +89,60 @@ async fn post_handler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn render_template(
|
async fn update_handler(State(state): State<AppState>) -> impl IntoResponse {
|
||||||
template_name: &str,
|
// Run git pull --autostash
|
||||||
state: &AppState,
|
let output = Command::new("git")
|
||||||
) -> Result<String, String> {
|
.arg("pull")
|
||||||
|
.arg("--autostash")
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match output {
|
||||||
|
Ok(output) => {
|
||||||
|
if !output.status.success() {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
eprintln!("Git pull failed: {}", stderr);
|
||||||
|
return (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("Git pull failed: {}", stderr),
|
||||||
|
)
|
||||||
|
.into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
println!("Git pull output: {}", stdout);
|
||||||
|
|
||||||
|
// Refresh posts
|
||||||
|
let mut manager = state.post_manager.write().await;
|
||||||
|
match manager.refresh_posts() {
|
||||||
|
Ok(_) => {
|
||||||
|
println!("Successfully refreshed log pages");
|
||||||
|
(StatusCode::OK, "Update successful: pulled changes and refreshed log pages")
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to refresh posts: {}", e);
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("Failed to refresh posts: {}", e),
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to execute git pull: {}", e);
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("Failed to execute git pull: {}", e),
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async fn render_template(template_name: &str, state: &AppState) -> Result<String, String> {
|
||||||
let template_path = format!("templates/{}", template_name);
|
let template_path = format!("templates/{}", template_name);
|
||||||
let template_content = std::fs::read_to_string(&template_path)
|
let template_content = std::fs::read_to_string(&template_path)
|
||||||
.map_err(|e| format!("Failed to read template: {}", e))?;
|
.map_err(|e| format!("Failed to read template: {}", e))?;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use crate::git_tracker::{get_git_version, GitVersion};
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use pulldown_cmark::{html, Options, Parser};
|
use pulldown_cmark::{html, Options, Parser};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -15,65 +14,53 @@ pub struct Post {
|
|||||||
pub markdown_content: String,
|
pub markdown_content: String,
|
||||||
pub html_content: String,
|
pub html_content: String,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub modified_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Entry {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
short_name: String,
|
||||||
|
file_path: String,
|
||||||
|
created_at: DateTime<Utc>,
|
||||||
|
modified_at: DateTime<Utc>,
|
||||||
|
status: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PostManager {
|
pub struct PostManager {
|
||||||
git_dir: PathBuf,
|
root_dir: PathBuf,
|
||||||
posts_dir: PathBuf,
|
posts_dir_rel: String,
|
||||||
posts: HashMap<String, Post>,
|
posts: HashMap<String, Post>,
|
||||||
last_git_version: Option<GitVersion>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PostManager {
|
impl PostManager {
|
||||||
pub fn new(root_dir: &str) -> Result<Self, String> {
|
pub fn new(root_dir: &str) -> Result<Self, String> {
|
||||||
let git_dir = PathBuf::from(root_dir);
|
let root_dir = PathBuf::from(root_dir);
|
||||||
let posts_dir = git_dir.join("posts");
|
let posts_dir_rel = "posts";
|
||||||
|
|
||||||
// Create posts directory if it doesn't exist
|
|
||||||
if !posts_dir.exists() {
|
|
||||||
fs::create_dir_all(&posts_dir)
|
|
||||||
.map_err(|e| format!("Failed to create posts directory: {}", e))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut manager = PostManager {
|
let mut manager = PostManager {
|
||||||
git_dir,
|
root_dir,
|
||||||
posts_dir,
|
posts_dir_rel: posts_dir_rel.to_string(),
|
||||||
posts: HashMap::new(),
|
posts: HashMap::new(),
|
||||||
last_git_version: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
manager.refresh_posts()?;
|
manager.refresh_posts()?;
|
||||||
Ok(manager)
|
Ok(manager)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn refresh_if_needed(&mut self) -> Result<bool, String> {
|
pub fn get_root_dir(&self) -> &PathBuf {
|
||||||
let current_version = get_git_version(self.git_dir.to_str().unwrap())?;
|
&self.root_dir
|
||||||
|
|
||||||
if self.last_git_version.as_ref() != Some(¤t_version) {
|
|
||||||
println!(
|
|
||||||
"Git version changed: {} -> {}",
|
|
||||||
self.last_git_version
|
|
||||||
.as_ref()
|
|
||||||
.map(|v| v.key())
|
|
||||||
.unwrap_or_else(|| "none".to_string()),
|
|
||||||
current_version.key()
|
|
||||||
);
|
|
||||||
self.refresh_posts()?;
|
|
||||||
self.last_git_version = Some(current_version);
|
|
||||||
Ok(true)
|
|
||||||
} else {
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_post_timestamps(&self) -> Result<HashMap<String, DateTime<Utc>>, String> {
|
fn query_posts(&self) -> Result<HashMap<String, Entry>, String> {
|
||||||
let output = Command::new("git")
|
let output = Command::new("git")
|
||||||
.arg("whatchanged")
|
.arg("log")
|
||||||
.arg("--pretty=%h - %cd - %s")
|
.arg("--raw")
|
||||||
|
.arg("--no-merges")
|
||||||
|
.arg("--pretty=%h - %ad - %s")
|
||||||
.arg("--date=unix")
|
.arg("--date=unix")
|
||||||
.arg("--")
|
.arg("--")
|
||||||
.arg("posts")
|
.arg(self.posts_dir_rel.clone())
|
||||||
.current_dir(&self.git_dir)
|
.current_dir(&self.root_dir)
|
||||||
.output()
|
.output()
|
||||||
.map_err(|e| format!("Failed to execute git whatchanged: {}", e))?;
|
.map_err(|e| format!("Failed to execute git whatchanged: {}", e))?;
|
||||||
|
|
||||||
@@ -85,8 +72,7 @@ impl PostManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let log_output = String::from_utf8_lossy(&output.stdout);
|
let log_output = String::from_utf8_lossy(&output.stdout);
|
||||||
let mut timestamps: HashMap<String, DateTime<Utc>> = HashMap::new();
|
let mut result: HashMap<String, Entry> = HashMap::new();
|
||||||
|
|
||||||
let mut current_timestamp: Option<DateTime<Utc>> = None;
|
let mut current_timestamp: Option<DateTime<Utc>> = None;
|
||||||
|
|
||||||
for line in log_output.lines() {
|
for line in log_output.lines() {
|
||||||
@@ -105,91 +91,70 @@ impl PostManager {
|
|||||||
let timestamp_str = after_first_dash[..second_dash_pos].trim();
|
let timestamp_str = after_first_dash[..second_dash_pos].trim();
|
||||||
if let Ok(timestamp) = timestamp_str.parse::<i64>() {
|
if let Ok(timestamp) = timestamp_str.parse::<i64>() {
|
||||||
current_timestamp = DateTime::from_timestamp(timestamp, 0);
|
current_timestamp = DateTime::from_timestamp(timestamp, 0);
|
||||||
|
eprintln!("Timestamp: {:?} ({})", current_timestamp, timestamp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse file change line: ":000000 100644 0000000 6bdad65 A posts/hello-world-2.md"
|
// Parse file change line: ":000000 100644 0000000 6bdad65 A posts/hello-world-2.md"
|
||||||
else if line.starts_with(':') {
|
if line.starts_with(':') {
|
||||||
|
eprintln!("Parsing line: {}", line);
|
||||||
if let Some(timestamp) = current_timestamp {
|
if let Some(timestamp) = current_timestamp {
|
||||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||||
if parts.len() >= 6 {
|
if parts.len() >= 6 {
|
||||||
let status = parts[4]; // A (add), D (delete), M (modify)
|
let status = parts[4]; // A (add), D (delete), M (modify)
|
||||||
let file_path = parts[5];
|
let file_path = parts[5];
|
||||||
|
|
||||||
// Only process existing files (not deleted ones)
|
let fn_ = file_path
|
||||||
if status != "D" && file_path.starts_with("posts/") {
|
.strip_prefix("posts/")
|
||||||
if let Some(file_name) = file_path.strip_prefix("posts/") {
|
.and_then(|s| s.strip_suffix(".md"));
|
||||||
if let Some(name) = file_name.strip_suffix(".md") {
|
if let Some(name) = fn_ {
|
||||||
// Only update if we don't have a timestamp yet (latest commit wins)
|
let entry = result.entry(name.to_string()).or_insert(Entry {
|
||||||
timestamps.entry(name.to_string()).or_insert(timestamp);
|
short_name: name.to_string(),
|
||||||
}
|
file_path: file_path.to_string(),
|
||||||
}
|
created_at: timestamp,
|
||||||
|
modified_at: timestamp,
|
||||||
|
status: if status == "D" { -1 } else { 1 },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Always use the oldest timestamp for posts creation dates
|
||||||
|
entry.created_at = timestamp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("Invalid git log output, expected prior timestamp: {}", line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(timestamps)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn refresh_posts(&mut self) -> Result<(), String> {
|
pub fn refresh_posts(&mut self) -> Result<(), String> {
|
||||||
self.posts.clear();
|
self.posts.clear();
|
||||||
|
|
||||||
// Get timestamps from git history
|
// Get timestamps from git history
|
||||||
let git_timestamps = self.get_post_timestamps()?;
|
let query_result = self.query_posts()?;
|
||||||
|
for (name, entry) in query_result {
|
||||||
let entries = fs::read_dir(&self.posts_dir)
|
if entry.status != 1 {
|
||||||
.map_err(|e| format!("Failed to read posts directory: {}", e))?;
|
continue;
|
||||||
|
}
|
||||||
for entry in entries {
|
let markdown_content = fs::read_to_string(&entry.file_path)
|
||||||
let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?;
|
|
||||||
let path = entry.path();
|
|
||||||
|
|
||||||
if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("md") {
|
|
||||||
let filename = path
|
|
||||||
.file_name()
|
|
||||||
.and_then(|s| s.to_str())
|
|
||||||
.ok_or_else(|| "Invalid filename".to_string())?
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let name = path
|
|
||||||
.file_stem()
|
|
||||||
.and_then(|s| s.to_str())
|
|
||||||
.ok_or_else(|| "Invalid file stem".to_string())?
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let markdown_content = fs::read_to_string(&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 = markdown_to_html(&markdown_content);
|
||||||
|
|
||||||
// Use git timestamp if available, otherwise fall back to file metadata
|
|
||||||
let created_at = git_timestamps
|
|
||||||
.get(&name)
|
|
||||||
.copied()
|
|
||||||
.or_else(|| {
|
|
||||||
let metadata = fs::metadata(&path).ok()?;
|
|
||||||
metadata
|
|
||||||
.created()
|
|
||||||
.or_else(|_| metadata.modified())
|
|
||||||
.ok()
|
|
||||||
.map(|t| DateTime::<Utc>::from(t))
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|| Utc::now());
|
|
||||||
|
|
||||||
let post = Post {
|
let post = Post {
|
||||||
name: name.clone(),
|
name: name.clone(),
|
||||||
filename,
|
filename: entry.file_path,
|
||||||
markdown_content,
|
markdown_content,
|
||||||
html_content,
|
html_content,
|
||||||
created_at,
|
created_at: entry.created_at,
|
||||||
|
modified_at: entry.modified_at,
|
||||||
};
|
};
|
||||||
|
eprintln!("Loaded post: {} ({})", name, entry.created_at);
|
||||||
self.posts.insert(name, post);
|
self.posts.insert(name, post);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
println!("Loaded {} posts", self.posts.len());
|
println!("Loaded {} posts", self.posts.len());
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -199,12 +164,24 @@ impl PostManager {
|
|||||||
self.posts.get(name)
|
self.posts.get(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get all posts, sorted by creation date
|
||||||
pub fn get_all_posts(&self) -> Vec<&Post> {
|
pub fn get_all_posts(&self) -> Vec<&Post> {
|
||||||
let mut posts: Vec<&Post> = self.posts.values().collect();
|
let mut posts: Vec<&Post> = self.posts.values().collect();
|
||||||
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
|
||||||
|
// 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
|
||||||
|
.first()
|
||||||
|
.ok_or("No posts found".to_string())?
|
||||||
|
.created_at)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_posts_limited(&self, limit: usize) -> Vec<&Post> {
|
pub fn get_posts_limited(&self, limit: usize) -> Vec<&Post> {
|
||||||
let mut posts = self.get_all_posts();
|
let mut posts = self.get_all_posts();
|
||||||
posts.truncate(limit);
|
posts.truncate(limit);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::post_manager::PostManager;
|
use crate::post_manager::PostManager;
|
||||||
use regex::Regex;
|
use crate::git_tracker::get_git_version;
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
pub fn render_template(
|
pub fn render_template(
|
||||||
@@ -7,83 +8,189 @@ pub fn render_template(
|
|||||||
post_manager: &PostManager,
|
post_manager: &PostManager,
|
||||||
current_post: Option<&str>,
|
current_post: Option<&str>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
let mut result = template.to_string();
|
parse_template(template, post_manager, current_post, 0)
|
||||||
|
|
||||||
// Process includes first
|
|
||||||
result = process_includes(&result)?;
|
|
||||||
|
|
||||||
// Process post-html tag
|
|
||||||
if let Some(post_name) = current_post {
|
|
||||||
result = process_post_html(&result, post_manager, post_name)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process post-list tag
|
fn parse_template(
|
||||||
result = process_post_list(&result, post_manager)?;
|
template: &str,
|
||||||
|
post_manager: &PostManager,
|
||||||
|
current_post: Option<&str>,
|
||||||
|
depth: usize,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
const MAX_DEPTH: usize = 10;
|
||||||
|
if depth > MAX_DEPTH {
|
||||||
|
return Err("Too many nested includes (max 10)".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut result = String::new();
|
||||||
|
let chars: Vec<char> = template.chars().collect();
|
||||||
|
let mut i = 0;
|
||||||
|
|
||||||
|
while i < chars.len() {
|
||||||
|
// Check for $< tag start
|
||||||
|
if i + 1 < chars.len() && chars[i] == '$' && chars[i + 1] == '<' {
|
||||||
|
// 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)?;
|
||||||
|
result.push_str(&content);
|
||||||
|
i = end_pos;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular character
|
||||||
|
result.push(chars[i]);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_includes(template: &str) -> Result<String, String> {
|
fn parse_tag(chars: &[char], start: usize) -> Option<(String, HashMap<String, String>, usize)> {
|
||||||
let include_regex = Regex::new(r#"\$<include\s+src="([^"]+)"\s*/>"#).unwrap();
|
let mut i = start + 2; // Skip $<
|
||||||
let mut result = template.to_string();
|
|
||||||
let mut iteration = 0;
|
// Skip whitespace
|
||||||
const MAX_ITERATIONS: usize = 10;
|
while i < chars.len() && chars[i].is_whitespace() {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse tag name
|
||||||
|
let mut tag_name = String::new();
|
||||||
|
while i < chars.len() && !chars[i].is_whitespace() && chars[i] != '/' && chars[i] != '>' {
|
||||||
|
tag_name.push(chars[i]);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if tag_name.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse attributes
|
||||||
|
let mut attrs = HashMap::new();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
iteration += 1;
|
// Skip whitespace
|
||||||
if iteration > MAX_ITERATIONS {
|
while i < chars.len() && chars[i].is_whitespace() {
|
||||||
return Err("Too many nested includes (max 10)".to_string());
|
i += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let result_copy = result.clone();
|
// Check for end of tag
|
||||||
let captures: Vec<_> = include_regex.captures_iter(&result_copy).collect();
|
if i >= chars.len() {
|
||||||
|
return None;
|
||||||
if captures.is_empty() {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for cap in captures {
|
if chars[i] == '/' {
|
||||||
let full_match = cap.get(0).unwrap().as_str();
|
i += 1;
|
||||||
let src = cap.get(1).unwrap().as_str();
|
if i < chars.len() && chars[i] == '>' {
|
||||||
|
return Some((tag_name, attrs, i + 1));
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if chars[i] == '>' {
|
||||||
|
return Some((tag_name, attrs, i + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse attribute name
|
||||||
|
let mut attr_name = String::new();
|
||||||
|
while i < chars.len() && chars[i] != '=' && !chars[i].is_whitespace() {
|
||||||
|
attr_name.push(chars[i]);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if attr_name.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip whitespace
|
||||||
|
while i < chars.len() && chars[i].is_whitespace() {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expect =
|
||||||
|
if i >= chars.len() || chars[i] != '=' {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
|
||||||
|
// Skip whitespace
|
||||||
|
while i < chars.len() && chars[i].is_whitespace() {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse attribute value (quoted)
|
||||||
|
if i >= chars.len() || chars[i] != '"' {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
i += 1; // Skip opening quote
|
||||||
|
|
||||||
|
let mut attr_value = String::new();
|
||||||
|
while i < chars.len() && chars[i] != '"' {
|
||||||
|
attr_value.push(chars[i]);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if i >= chars.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
i += 1; // Skip closing quote
|
||||||
|
|
||||||
|
attrs.insert(attr_name, attr_value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_tag(
|
||||||
|
tag_name: &str,
|
||||||
|
attrs: &HashMap<String, String>,
|
||||||
|
post_manager: &PostManager,
|
||||||
|
current_post: Option<&str>,
|
||||||
|
depth: usize,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
match tag_name {
|
||||||
|
"include" => {
|
||||||
|
let src = attrs.get("src")
|
||||||
|
.ok_or_else(|| "include tag missing 'src' attribute".to_string())?;
|
||||||
|
|
||||||
let include_path = format!("templates/{}", src);
|
let include_path = format!("templates/{}", src);
|
||||||
let content = fs::read_to_string(&include_path)
|
let content = fs::read_to_string(&include_path)
|
||||||
.map_err(|e| format!("Failed to read include file '{}': {}", src, e))?;
|
.map_err(|e| format!("Failed to read include file '{}': {}", src, e))?;
|
||||||
|
|
||||||
result = result.replace(full_match, &content);
|
// Recursively parse the included content
|
||||||
}
|
parse_template(&content, post_manager, current_post, depth + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(result)
|
"post-html" => {
|
||||||
}
|
let post_name = current_post
|
||||||
|
.ok_or_else(|| "post-html tag used outside of post context".to_string())?;
|
||||||
fn process_post_html(
|
|
||||||
template: &str,
|
|
||||||
post_manager: &PostManager,
|
|
||||||
post_name: &str,
|
|
||||||
) -> Result<String, String> {
|
|
||||||
let post_html_regex = Regex::new(r"\$<post-html\s*/?>").unwrap();
|
|
||||||
|
|
||||||
let post = post_manager
|
let post = post_manager
|
||||||
.get_post(post_name)
|
.get_post(post_name)
|
||||||
.ok_or_else(|| format!("Post '{}' not found", post_name))?;
|
.ok_or_else(|| format!("Post '{}' not found", post_name))?;
|
||||||
|
|
||||||
Ok(post_html_regex.replace_all(template, post.html_content.as_str()).to_string())
|
Ok(post.html_content.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_post_list(
|
"updated" => {
|
||||||
template: &str,
|
// Insert the last updated time of the current post (or most recent post)
|
||||||
post_manager: &PostManager,
|
let post_ts = if let Some(p) = current_post {
|
||||||
) -> Result<String, String> {
|
post_manager.get_post(p).ok_or_else(|| format!("Post '{}' not found", p))?.modified_at
|
||||||
let post_list_regex = Regex::new(r#"\$<post-list(?:\s+limit=(\d+))?\s*/>"#).unwrap();
|
} else {
|
||||||
|
post_manager.get_update_timestamp()?
|
||||||
|
};
|
||||||
|
|
||||||
let mut result = template.to_string();
|
Ok(post_ts.format("%Y-%m-%d").to_string())
|
||||||
|
}
|
||||||
|
|
||||||
for cap in post_list_regex.captures_iter(template) {
|
"version" => {
|
||||||
let full_match = cap.get(0).unwrap().as_str();
|
let git_dir = post_manager.get_root_dir().clone();
|
||||||
let limit_attr = cap.get(1).and_then(|m| m.as_str().parse::<usize>().ok());
|
let git_version = get_git_version(&git_dir)?;
|
||||||
|
Ok(format!("{}/{}", git_version.branch, git_version.commit))
|
||||||
|
}
|
||||||
|
|
||||||
let limit = limit_attr;
|
"post-list" => {
|
||||||
|
let limit = attrs.get("limit")
|
||||||
|
.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)
|
||||||
@@ -102,10 +209,11 @@ fn process_post_list(
|
|||||||
}
|
}
|
||||||
list_html.push_str("</ul>");
|
list_html.push_str("</ul>");
|
||||||
|
|
||||||
result = result.replace(full_match, &list_html);
|
Ok(list_html)
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(result)
|
_ => Err(format!("Unknown tag: {}", tag_name))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -113,26 +221,50 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_post_list_regex() {
|
fn test_parse_tag_post_list() {
|
||||||
let regex = Regex::new(r#"\$<post-list(?:\s+limit=(\d+))?\s*/>"#).unwrap();
|
let chars: Vec<char> = "$<post-list/>".chars().collect();
|
||||||
|
let result = parse_tag(&chars, 0);
|
||||||
|
assert!(result.is_some());
|
||||||
|
let (tag_name, attrs, _) = result.unwrap();
|
||||||
|
assert_eq!(tag_name, "post-list");
|
||||||
|
assert!(attrs.is_empty());
|
||||||
|
|
||||||
assert!(regex.is_match("$<post-list/>"));
|
let chars: Vec<char> = "$<post-list />".chars().collect();
|
||||||
assert!(regex.is_match("$<post-list />"));
|
let result = parse_tag(&chars, 0);
|
||||||
assert!(regex.is_match("$<post-list limit=50/>"));
|
assert!(result.is_some());
|
||||||
assert!(regex.is_match("$<post-list limit=50 />"));
|
|
||||||
|
|
||||||
let cap = regex.captures("$<post-list limit=50/>").unwrap();
|
let chars: Vec<char> = r#"$<post-list limit="50"/>"#.chars().collect();
|
||||||
assert_eq!(cap.get(1).unwrap().as_str(), "50");
|
let result = parse_tag(&chars, 0);
|
||||||
|
assert!(result.is_some());
|
||||||
|
let (tag_name, attrs, _) = result.unwrap();
|
||||||
|
assert_eq!(tag_name, "post-list");
|
||||||
|
assert_eq!(attrs.get("limit"), Some(&"50".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_include_regex() {
|
fn test_parse_tag_include() {
|
||||||
let regex = Regex::new(r#"\$<include\s+src="([^"]+)"\s*/>"#).unwrap();
|
let chars: Vec<char> = r#"$<include src="header.html"/>"#.chars().collect();
|
||||||
|
let result = parse_tag(&chars, 0);
|
||||||
|
assert!(result.is_some());
|
||||||
|
let (tag_name, attrs, _) = result.unwrap();
|
||||||
|
assert_eq!(tag_name, "include");
|
||||||
|
assert_eq!(attrs.get("src"), Some(&"header.html".to_string()));
|
||||||
|
|
||||||
assert!(regex.is_match(r#"$<include src="header.html"/>"#));
|
let chars: Vec<char> = r#"$<include src="header.html" />"#.chars().collect();
|
||||||
assert!(regex.is_match(r#"$<include src="header.html" />"#));
|
let result = parse_tag(&chars, 0);
|
||||||
|
assert!(result.is_some());
|
||||||
|
let (tag_name, attrs, _) = result.unwrap();
|
||||||
|
assert_eq!(tag_name, "include");
|
||||||
|
assert_eq!(attrs.get("src"), Some(&"header.html".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
let cap = regex.captures(r#"$<include src="header.html"/>"#).unwrap();
|
#[test]
|
||||||
assert_eq!(cap.get(1).unwrap().as_str(), "header.html");
|
fn test_parse_tag_post_html() {
|
||||||
|
let chars: Vec<char> = "$<post-html/>".chars().collect();
|
||||||
|
let result = parse_tag(&chars, 0);
|
||||||
|
assert!(result.is_some());
|
||||||
|
let (tag_name, attrs, _) = result.unwrap();
|
||||||
|
assert_eq!(tag_name, "post-html");
|
||||||
|
assert!(attrs.is_empty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<footer style="margin-top: 50px; padding-top: 20px; border-top: 1px solid #ddd; color: #666; text-align: center;">
|
<footer style="margin-top: 50px; padding-top: 20px; border-top: 1px solid #ddd; color: #666; text-align: center;">
|
||||||
<p>My Static Blog - Powered by Rust and CommonMark</p>
|
<p>
|
||||||
|
Guus Waals - updated on $<updated/> </br>
|
||||||
|
$<version/>
|
||||||
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -74,6 +74,20 @@
|
|||||||
pre code {
|
pre code {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
.github-link {
|
||||||
|
float: right;
|
||||||
|
display: inline-block;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.github-link svg {
|
||||||
|
fill: #333;
|
||||||
|
transition: fill 0.2s;
|
||||||
|
}
|
||||||
|
.github-link:hover svg {
|
||||||
|
fill: #0066cc;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -81,4 +95,9 @@
|
|||||||
<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">
|
||||||
|
<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>
|
||||||
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
$<include src="header.html"/>
|
$<include src="header.html"/>
|
||||||
<h1>Recent Posts</h1>
|
<h1>Recent Posts</h1>
|
||||||
$<post-list limit=2/>
|
$<post-list limit="10"/>
|
||||||
$<include src="footer.html"/>
|
$<include src="footer.html"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user