added the changelog command
This commit is contained in:
@@ -45,3 +45,8 @@ Or you can generate a static build with:
|
|||||||
```sh
|
```sh
|
||||||
wiki-maker --path ./example/ build -o ./dist/
|
wiki-maker --path ./example/ build -o ./dist/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You can generate and update a changelog page based on git history with:
|
||||||
|
```sh
|
||||||
|
wiki-maker --path ./example/ changelog gen-from-git
|
||||||
|
```
|
||||||
|
|||||||
52
example/_changelog.toml
Normal file
52
example/_changelog.toml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# This file was generated by `wiki-maker changelog gen-from-git`
|
||||||
|
|
||||||
|
[[entries]]
|
||||||
|
date = "2026-02-26 09:06:06"
|
||||||
|
message = "fixed and improved images, and generalized renderer"
|
||||||
|
author = "eiiko6"
|
||||||
|
hash = "b854e4b633326f020005b8ae67b99da21788408a"
|
||||||
|
files = ["images/bernardo.png"]
|
||||||
|
|
||||||
|
[[entries]]
|
||||||
|
date = "2026-02-13 18:20:33"
|
||||||
|
message = "converted to wiki"
|
||||||
|
author = "eiiko6"
|
||||||
|
hash = "0ca4161e036d2735f15aefc87def7ec8bab0902d"
|
||||||
|
files = [
|
||||||
|
"azrak.md",
|
||||||
|
"azrak.toml",
|
||||||
|
"bernardo.md",
|
||||||
|
"bernardo.png",
|
||||||
|
"bernardo.toml",
|
||||||
|
"coffee@1767815445.md",
|
||||||
|
"ferris@1767468388.md",
|
||||||
|
"lumina.md",
|
||||||
|
"lumina.toml",
|
||||||
|
"space@1767813388.md",
|
||||||
|
"voidmother.md",
|
||||||
|
"voidmother.toml",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[entries]]
|
||||||
|
date = "2026-01-07 20:55:32"
|
||||||
|
message = "implemented automatic summary generation, and added timestamps in page list"
|
||||||
|
author = "eiiko6"
|
||||||
|
hash = "dcae9feac795bd338c30d11a29d20834ff4101cd"
|
||||||
|
files = [
|
||||||
|
"SUMMARY.md",
|
||||||
|
"coffee@1767815445.md",
|
||||||
|
"ferris@1767468388.md",
|
||||||
|
"space@1767813388.md",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[entries]]
|
||||||
|
date = "2026-01-07 18:29:07"
|
||||||
|
message = "base markdown book with code block syntax highlighting and fully embedded assets"
|
||||||
|
author = "eiiko6"
|
||||||
|
hash = "1c4e99b138a17eb6368c097df0945928c26e0b5d"
|
||||||
|
files = [
|
||||||
|
"01-ferris.md",
|
||||||
|
"02-coffee.md",
|
||||||
|
"03-space.md",
|
||||||
|
"SUMMARY.md",
|
||||||
|
]
|
||||||
109
flake.lock
generated
Normal file
109
flake.lock
generated
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"code-theme": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"narHash": "sha256-28LGXKITHbrmt6qrcG/W+qTlaWthre7x7izp/TPjQgA=",
|
||||||
|
"type": "file",
|
||||||
|
"url": "https://raw.githubusercontent.com/catppuccin/bat/refs/heads/main/themes/Catppuccin%20Macchiato.tmTheme"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"type": "file",
|
||||||
|
"url": "https://raw.githubusercontent.com/catppuccin/bat/refs/heads/main/themes/Catppuccin%20Macchiato.tmTheme"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1771848320,
|
||||||
|
"narHash": "sha256-0MAd+0mun3K/Ns8JATeHT1sX28faLII5hVLq0L3BdZU=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "2fc6539b481e1d2569f25f8799236694180c0993",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1744536153,
|
||||||
|
"narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"code-theme": "code-theme",
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"rust-overlay": "rust-overlay"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rust-overlay": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs_2"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1772075164,
|
||||||
|
"narHash": "sha256-93XcvAt+6p7aAq1ERlxD2T17zLGoYGo64KJYasGcpgc=",
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"rev": "07601339b15fa6810541c0e7dc2f3664d92a7ad0",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
191
src/changelog.rs
Normal file
191
src/changelog.rs
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
use anyhow::{Context, Result, anyhow};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::Command;
|
||||||
|
use tokio::fs;
|
||||||
|
|
||||||
|
use crate::cli::ChangelogCommands;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct ChangelogEntry {
|
||||||
|
pub date: String,
|
||||||
|
pub message: String,
|
||||||
|
pub author: String,
|
||||||
|
pub hash: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub files: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||||
|
pub struct ChangelogConfig {
|
||||||
|
pub entries: Vec<ChangelogEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle(cmd: ChangelogCommands, root_dir: PathBuf) -> Result<()> {
|
||||||
|
let changelog_path = root_dir.join("_changelog.toml");
|
||||||
|
|
||||||
|
match cmd {
|
||||||
|
ChangelogCommands::View => {
|
||||||
|
if !changelog_path.exists() {
|
||||||
|
println!("No changelog found at {:?}", changelog_path);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let content = fs::read_to_string(&changelog_path).await?;
|
||||||
|
let config: ChangelogConfig = toml::from_str(&content)?;
|
||||||
|
|
||||||
|
println!("Changelog ({} entries):", config.entries.len());
|
||||||
|
for entry in config.entries {
|
||||||
|
println!("- [{}] {} ({})", entry.date, entry.message, entry.author);
|
||||||
|
if !entry.files.is_empty() {
|
||||||
|
println!(" Files: {:?}\n", entry.files);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ChangelogCommands::GenFromGit { git_path } => {
|
||||||
|
let git_dir = git_path.unwrap_or_else(|| root_dir.clone());
|
||||||
|
update_from_git(&changelog_path, &git_dir, &root_dir).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_from_git(changelog_path: &Path, git_dir: &Path, wiki_root: &Path) -> Result<()> {
|
||||||
|
let output_root = Command::new("git")
|
||||||
|
.arg("-C")
|
||||||
|
.arg(git_dir)
|
||||||
|
.args(["rev-parse", "--show-toplevel"])
|
||||||
|
.output()
|
||||||
|
.context("Failed to find git root")?;
|
||||||
|
|
||||||
|
let repo_root_str = String::from_utf8(output_root.stdout)?.trim().to_string();
|
||||||
|
let repo_root = PathBuf::from(&repo_root_str);
|
||||||
|
|
||||||
|
let prefix_path = wiki_root.strip_prefix(&repo_root).unwrap_or(Path::new(""));
|
||||||
|
|
||||||
|
let mut prefix = prefix_path.to_string_lossy().to_string().replace('\\', "/");
|
||||||
|
if !prefix.is_empty() && !prefix.ends_with('/') {
|
||||||
|
prefix.push('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load existing
|
||||||
|
let mut config = if changelog_path.exists() {
|
||||||
|
let content = fs::read_to_string(changelog_path).await?;
|
||||||
|
toml::from_str(&content).unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
ChangelogConfig::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let existing_hashes: HashSet<String> = config.entries.iter().map(|e| e.hash.clone()).collect();
|
||||||
|
|
||||||
|
let output = Command::new("git")
|
||||||
|
.arg("-C")
|
||||||
|
.arg(git_dir)
|
||||||
|
.args([
|
||||||
|
"log",
|
||||||
|
"--name-only",
|
||||||
|
"--pretty=format:---ENTRY---|%H|%ad|%an|%s",
|
||||||
|
"--date=format:%Y-%m-%d %H:%M:%S",
|
||||||
|
"--",
|
||||||
|
".",
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.context("Failed to execute git command")?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"Git command failed: {}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = String::from_utf8(output.stdout)?;
|
||||||
|
let mut new_entries = Vec::new();
|
||||||
|
let mut current_entry: Option<ChangelogEntry> = None;
|
||||||
|
|
||||||
|
for line in stdout.lines() {
|
||||||
|
if line.starts_with("---ENTRY---|") {
|
||||||
|
// Push previous
|
||||||
|
if let Some(entry) = current_entry.take() {
|
||||||
|
if !entry.files.is_empty() && !existing_hashes.contains(&entry.hash) {
|
||||||
|
new_entries.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse new
|
||||||
|
let parts: Vec<&str> = line.split('|').collect();
|
||||||
|
if parts.len() >= 5 {
|
||||||
|
let hash = parts[1].to_string();
|
||||||
|
let date = parts[2].to_string();
|
||||||
|
let author = parts[3].to_string();
|
||||||
|
let message = parts[4..].join("|");
|
||||||
|
|
||||||
|
current_entry = Some(ChangelogEntry {
|
||||||
|
hash,
|
||||||
|
date,
|
||||||
|
author,
|
||||||
|
message,
|
||||||
|
files: Vec::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if !line.trim().is_empty() {
|
||||||
|
let raw_path = line.trim();
|
||||||
|
|
||||||
|
// Check if file belongs to the wiki directory
|
||||||
|
if prefix.is_empty() || raw_path.starts_with(&prefix) {
|
||||||
|
if let Some(entry) = current_entry.as_mut() {
|
||||||
|
// Strip the prefix to get the relative path inside the wiki
|
||||||
|
let clean_path = if prefix.is_empty() {
|
||||||
|
raw_path.to_string()
|
||||||
|
} else {
|
||||||
|
raw_path
|
||||||
|
.strip_prefix(&prefix)
|
||||||
|
.unwrap_or(raw_path)
|
||||||
|
.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
if clean_path != "changelog.toml" {
|
||||||
|
entry.files.push(clean_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push last
|
||||||
|
if let Some(entry) = current_entry {
|
||||||
|
if !entry.files.is_empty() && !existing_hashes.contains(&entry.hash) {
|
||||||
|
new_entries.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if new_entries.is_empty() {
|
||||||
|
println!("No new commits touching the wiki directory found.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Found {} new commits.", new_entries.len());
|
||||||
|
|
||||||
|
config.entries.extend(new_entries);
|
||||||
|
config.entries.sort_by(|a, b| b.date.cmp(&a.date));
|
||||||
|
|
||||||
|
let mut toml_str =
|
||||||
|
String::from("# This file was generated by `wiki-maker changelog gen-from-git`\n\n");
|
||||||
|
toml_str.push_str(&toml::to_string_pretty(&config)?);
|
||||||
|
fs::write(changelog_path, toml_str).await?;
|
||||||
|
println!("Updated {:?}", changelog_path);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn load_changelog(root_dir: &Path) -> Vec<ChangelogEntry> {
|
||||||
|
let path = root_dir.join("_changelog.toml");
|
||||||
|
if path.exists() {
|
||||||
|
if let Ok(content) = fs::read_to_string(path).await {
|
||||||
|
if let Ok(config) = toml::from_str::<ChangelogConfig>(&content) {
|
||||||
|
return config.entries;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
18
src/cli.rs
18
src/cli.rs
@@ -45,6 +45,11 @@ pub enum Commands {
|
|||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
cmd: EntryCommands,
|
cmd: EntryCommands,
|
||||||
},
|
},
|
||||||
|
/// Manage the changelog
|
||||||
|
Changelog {
|
||||||
|
#[command(subcommand)]
|
||||||
|
cmd: ChangelogCommands,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
@@ -67,3 +72,16 @@ pub enum EntryCommands {
|
|||||||
name: String,
|
name: String,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
pub enum ChangelogCommands {
|
||||||
|
/// View the current changelog
|
||||||
|
View,
|
||||||
|
/// Generate or update the changelog from git history
|
||||||
|
GenFromGit {
|
||||||
|
/// Path to the directory containing `.git/` (defaults to current dir or wiki dir).
|
||||||
|
/// Note that git will look in parent directories until it finds a `.git/`
|
||||||
|
#[arg(long)]
|
||||||
|
git_path: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
35
src/main.rs
35
src/main.rs
@@ -12,13 +12,16 @@ use syntect::{highlighting::ThemeSet, parsing::SyntaxSet};
|
|||||||
use tera::{Context, Tera};
|
use tera::{Context, Tera};
|
||||||
|
|
||||||
mod analysis;
|
mod analysis;
|
||||||
|
mod changelog;
|
||||||
mod cli;
|
mod cli;
|
||||||
mod entry;
|
mod entry;
|
||||||
mod rendering;
|
mod rendering;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::{Cli, Commands},
|
cli::{Cli, Commands},
|
||||||
rendering::{render_page_handler, render_summary_handler, render_wiki_page},
|
rendering::{
|
||||||
|
render_changelog_handler, render_page_handler, render_summary_handler, render_wiki_page,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -31,6 +34,10 @@ lazy_static! {
|
|||||||
("home.html", include_str!("../templates/home.html")),
|
("home.html", include_str!("../templates/home.html")),
|
||||||
("page.html", include_str!("../templates/page.html")),
|
("page.html", include_str!("../templates/page.html")),
|
||||||
("style.css", include_str!("../templates/style.css")),
|
("style.css", include_str!("../templates/style.css")),
|
||||||
|
(
|
||||||
|
"changelog.html",
|
||||||
|
include_str!("../templates/changelog.html"),
|
||||||
|
),
|
||||||
])
|
])
|
||||||
.unwrap();
|
.unwrap();
|
||||||
tera
|
tera
|
||||||
@@ -98,6 +105,7 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
.route("/", get(render_summary_handler))
|
.route("/", get(render_summary_handler))
|
||||||
.route("/{page}", get(render_page_handler))
|
.route("/{page}", get(render_page_handler))
|
||||||
.route("/style.css", get(serve_css))
|
.route("/style.css", get(serve_css))
|
||||||
|
.route("/changelog", get(render_changelog_handler))
|
||||||
.nest_service(
|
.nest_service(
|
||||||
"/images",
|
"/images",
|
||||||
tower_http::services::ServeDir::new(&shared_state.docs_dir.join("images")),
|
tower_http::services::ServeDir::new(&shared_state.docs_dir.join("images")),
|
||||||
@@ -139,6 +147,9 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
Commands::Entry { cmd } => {
|
Commands::Entry { cmd } => {
|
||||||
entry::handle(cmd, abs_path).await?;
|
entry::handle(cmd, abs_path).await?;
|
||||||
}
|
}
|
||||||
|
Commands::Changelog { cmd } => {
|
||||||
|
changelog::handle(cmd, abs_path).await?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -148,7 +159,9 @@ async fn get_summary_data(docs_dir: &PathBuf, is_static: bool) -> Vec<Page> {
|
|||||||
if let Ok(mut entries) = tokio::fs::read_dir(docs_dir).await {
|
if let Ok(mut entries) = tokio::fs::read_dir(docs_dir).await {
|
||||||
while let Ok(Some(entry)) = entries.next_entry().await {
|
while let Ok(Some(entry)) = entries.next_entry().await {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
if path.extension().and_then(|s| s.to_str()) != Some("toml") {
|
if path.extension().and_then(|s| s.to_str()) != Some("toml")
|
||||||
|
|| path.file_name().and_then(|s| s.to_str()) == Some("_changelog.toml")
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,6 +197,8 @@ async fn get_summary_data(docs_dir: &PathBuf, is_static: bool) -> Vec<Page> {
|
|||||||
async fn run_build(docs_dir: PathBuf, out_dir: PathBuf, no_navigation: bool) -> anyhow::Result<()> {
|
async fn run_build(docs_dir: PathBuf, out_dir: PathBuf, no_navigation: bool) -> anyhow::Result<()> {
|
||||||
tracing::info!("Building static site to: {}", out_dir.display());
|
tracing::info!("Building static site to: {}", out_dir.display());
|
||||||
|
|
||||||
|
let changelog_entries = changelog::load_changelog(&docs_dir).await;
|
||||||
|
|
||||||
if !no_navigation {
|
if !no_navigation {
|
||||||
let pages = get_summary_data(&docs_dir, true).await;
|
let pages = get_summary_data(&docs_dir, true).await;
|
||||||
let static_pages: Vec<Page> = pages
|
let static_pages: Vec<Page> = pages
|
||||||
@@ -194,22 +209,34 @@ async fn run_build(docs_dir: PathBuf, out_dir: PathBuf, no_navigation: bool) ->
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
// Render home page
|
||||||
let mut context = Context::new();
|
let mut context = Context::new();
|
||||||
context.insert("title", "Wiki Index");
|
context.insert("title", "Wiki Index");
|
||||||
context.insert("files", &static_pages);
|
context.insert("files", &static_pages);
|
||||||
|
context.insert("changelog", &changelog_entries);
|
||||||
context.insert("is_static", &true);
|
context.insert("is_static", &true);
|
||||||
|
|
||||||
let rendered = TEMPLATES.render("home.html", &context)?;
|
let rendered = TEMPLATES.render("home.html", &context)?;
|
||||||
tokio::fs::write(out_dir.join("index.html"), rendered).await?;
|
tokio::fs::write(out_dir.join("index.html"), rendered).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render changelog page
|
||||||
|
let mut context = Context::new();
|
||||||
|
context.insert("title", "Changelog");
|
||||||
|
context.insert("changelog", &changelog_entries);
|
||||||
|
context.insert("is_static", &true);
|
||||||
|
context.insert("no_navigation", &false);
|
||||||
|
let rendered = TEMPLATES.render("changelog.html", &context)?;
|
||||||
|
tokio::fs::write(out_dir.join("changelog.html"), &rendered).await?;
|
||||||
|
|
||||||
let css = TEMPLATES.render("style.css", &Context::new())?;
|
let css = TEMPLATES.render("style.css", &Context::new())?;
|
||||||
tokio::fs::write(out_dir.join("style.css"), css).await?;
|
tokio::fs::write(out_dir.join("style.css"), css).await?;
|
||||||
|
|
||||||
let mut entries = tokio::fs::read_dir(&docs_dir).await?;
|
let mut entries = tokio::fs::read_dir(&docs_dir).await?;
|
||||||
while let Some(entry) = entries.next_entry().await? {
|
while let Some(entry) = entries.next_entry().await? {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
if path.extension().and_then(|s| s.to_str()) == Some("toml") {
|
if path.extension().and_then(|s| s.to_str()) == Some("toml")
|
||||||
|
&& path.file_name().and_then(|s| s.to_str()) != Some("_changelog.toml")
|
||||||
|
{
|
||||||
let filename = if let Some(file_name) = entry.file_name().to_str() {
|
let filename = if let Some(file_name) = entry.file_name().to_str() {
|
||||||
file_name.to_string()
|
file_name.to_string()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ use syntect::html::highlighted_html_for_string;
|
|||||||
use tera::Context;
|
use tera::Context;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AppState, SYNTAX_SET, TEMPLATES, THEME_SET, WikiConfig, get_nav_links, get_summary_data,
|
AppState, SYNTAX_SET, TEMPLATES, THEME_SET, WikiConfig, changelog, get_nav_links,
|
||||||
|
get_summary_data,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Renderer<'a> {
|
pub struct Renderer<'a> {
|
||||||
@@ -202,10 +203,14 @@ pub async fn render_summary_handler(State(state): State<Arc<AppState>>) -> impl
|
|||||||
if state.no_navigation {
|
if state.no_navigation {
|
||||||
return (StatusCode::NOT_FOUND, "Disabled").into_response();
|
return (StatusCode::NOT_FOUND, "Disabled").into_response();
|
||||||
}
|
}
|
||||||
|
|
||||||
let pages = get_summary_data(&state.docs_dir, false).await;
|
let pages = get_summary_data(&state.docs_dir, false).await;
|
||||||
|
let changelog_entries = changelog::load_changelog(&state.docs_dir).await;
|
||||||
|
|
||||||
let mut context = Context::new();
|
let mut context = Context::new();
|
||||||
context.insert("title", "Wiki Index");
|
context.insert("title", "Wiki Index");
|
||||||
context.insert("files", &pages);
|
context.insert("files", &pages);
|
||||||
|
context.insert("changelog", &changelog_entries);
|
||||||
context.insert("is_static", &false);
|
context.insert("is_static", &false);
|
||||||
|
|
||||||
match TEMPLATES.render("home.html", &context) {
|
match TEMPLATES.render("home.html", &context) {
|
||||||
@@ -229,3 +234,22 @@ pub async fn render_page_handler(
|
|||||||
Err(e) => (StatusCode::NOT_FOUND, format!("<h1>404</h1><p>{}</p>", e)).into_response(),
|
Err(e) => (StatusCode::NOT_FOUND, format!("<h1>404</h1><p>{}</p>", e)).into_response(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn render_changelog_handler(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||||
|
if state.no_navigation {
|
||||||
|
return (StatusCode::NOT_FOUND, "Disabled").into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
let changelog_entries = changelog::load_changelog(&state.docs_dir).await;
|
||||||
|
|
||||||
|
let mut context = Context::new();
|
||||||
|
context.insert("title", "Changelog");
|
||||||
|
context.insert("changelog", &changelog_entries);
|
||||||
|
context.insert("is_static", &false);
|
||||||
|
context.insert("no_navigation", &false);
|
||||||
|
|
||||||
|
match TEMPLATES.render("changelog.html", &context) {
|
||||||
|
Ok(rendered) => Html(rendered).into_response(),
|
||||||
|
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
44
templates/changelog.html
Normal file
44
templates/changelog.html
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{% extends "_base.html" %}
|
||||||
|
{% block title %}{{ title }}{% endblock title %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="wiki-header">
|
||||||
|
<h1>{{ title }}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wiki-container">
|
||||||
|
<main class="wiki-body">
|
||||||
|
{% if changelog | length == 0 %}
|
||||||
|
<p>No changelog entries found.</p>
|
||||||
|
{% else %}
|
||||||
|
<ul class="changelog-list">
|
||||||
|
{% for entry in changelog %}
|
||||||
|
<li class="changelog-entry">
|
||||||
|
<div class="changelog-meta">
|
||||||
|
{# Split date "2026-02-26 15:10:00" into parts #}
|
||||||
|
{% set date_parts = entry.date | split(pat=" ") %}
|
||||||
|
<span class="changelog-day">{{ date_parts[0] }}</span>
|
||||||
|
<span class="changelog-time">{{ date_parts[1] }}</span>
|
||||||
|
<span class="changelog-author">by {{ entry.author }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="changelog-message">{{ entry.message }}</div>
|
||||||
|
|
||||||
|
{% if entry.files %}
|
||||||
|
<div class="changelog-files">
|
||||||
|
<details>
|
||||||
|
<summary>{{ entry.files | length }} file(s) changed</summary>
|
||||||
|
<ul>
|
||||||
|
{% for file in entry.files %}
|
||||||
|
<li>{{ file }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
||||||
@@ -16,6 +16,21 @@
|
|||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
|
<h2>Recent Changes</h2>
|
||||||
|
{% if changelog | length > 0 %}
|
||||||
|
<p><a href="{% if is_static %}changelog.html{% else %}changelog{% endif %}">View full changelog</a></p>
|
||||||
|
<ul>
|
||||||
|
{% for entry in changelog | slice(end=5) %}
|
||||||
|
<li>
|
||||||
|
<code style="color: var(--accent)">{{ entry.date | truncate(length=10, end="") }}</code>
|
||||||
|
{{ entry.message }}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p>No recent changes recorded.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
const dateElements = document.querySelectorAll('.local-date');
|
const dateElements = document.querySelectorAll('.local-date');
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ body {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: var(--container-width);
|
max-width: var(--container-width);
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
|
padding-bottom: 35px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Wiki Layout --- */
|
/* --- Wiki Layout --- */
|
||||||
@@ -171,3 +172,72 @@ nav {
|
|||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
nav a { font-weight: bold; font-size: 1.2rem; color: var(--text-main); }
|
nav a { font-weight: bold; font-size: 1.2rem; color: var(--text-main); }
|
||||||
|
|
||||||
|
.changelog-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.changelog-entry {
|
||||||
|
background: var(--container-bg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
.changelog-meta {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
padding-bottom: 5px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
.changelog-day {
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.changelog-time {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
.changelog-author {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
.changelog-message {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.changelog-files {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.changelog-files summary {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-muted);
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
.changelog-files summary:hover {
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
.changelog-files summary::before {
|
||||||
|
content: '▶';
|
||||||
|
font-size: 0.7em;
|
||||||
|
margin-right: 5px;
|
||||||
|
display: inline-block;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
.changelog-files details[open] summary::before {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-files ul {
|
||||||
|
margin: 5px 0 0 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user