From d1040954cbe7fd8ed65ab4cb412d2c4b955a34be Mon Sep 17 00:00:00 2001 From: eiiko6 Date: Sat, 21 Feb 2026 14:34:05 +0100 Subject: [PATCH] added graph and todo commands, and reorganized project structure --- Cargo.lock | 97 +++++++++++---------- src/analysis.rs | 132 ++++++++++++++++++++++++++++ src/cli.rs | 44 ++++++++++ src/main.rs | 219 ++++++++--------------------------------------- src/rendering.rs | 129 ++++++++++++++++++++++++++++ 5 files changed, 388 insertions(+), 233 deletions(-) create mode 100644 src/analysis.rs create mode 100644 src/cli.rs create mode 100644 src/rendering.rs diff --git a/Cargo.lock b/Cargo.lock index 63835d9..6e82eff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,9 +78,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.101" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "atomic-waker" @@ -96,9 +96,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.15.4" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256" +checksum = "d9a7b350e3bb1767102698302bc37256cbd48422809984b98d292c40e2579aa9" dependencies = [ "aws-lc-sys", "zeroize", @@ -185,9 +185,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "block-buffer" @@ -210,9 +210,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytes" @@ -287,9 +287,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.58" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" dependencies = [ "clap_builder", "clap_derive", @@ -297,9 +297,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.58" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" dependencies = [ "anstream", "anstyle", @@ -431,9 +431,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc3dc5ad92c2e2d1c193bbbbdf2ea477cb81331de4f3103f267ca18368b988c4" +checksum = "2163a0e204a148662b6b6816d4b5d5668a5f2f8df498ccbd5cd0e864e78fecba" dependencies = [ "powerfmt", ] @@ -535,9 +535,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -545,33 +545,33 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", "futures-io", @@ -579,7 +579,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -1024,9 +1023,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "93f0862381daaec758576dcc22eb7bbf4d7efd67328553f3b45a412a51a3fb21" dependencies = [ "once_cell", "wasm-bindgen", @@ -1748,9 +1747,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "3.5.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags", "core-foundation 0.10.1", @@ -1761,9 +1760,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -1944,9 +1943,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.115" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e614ed320ac28113fa64972c4262d5dbc89deacdfd00c34a3e4cea073243c12" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -2374,9 +2373,9 @@ checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-ident" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" @@ -2468,9 +2467,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "1de241cdc66a9d91bd84f097039eb140cdc6eec47e0cdbaf9d932a1dd6c35866" dependencies = [ "cfg-if", "once_cell", @@ -2481,9 +2480,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.58" +version = "0.4.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +checksum = "a42e96ea38f49b191e08a1bab66c7ffdba24b06f9995b39a9dd60222e5b6f1da" dependencies = [ "cfg-if", "futures-util", @@ -2495,9 +2494,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "e12fdf6649048f2e3de6d7d5ff3ced779cdedee0e0baffd7dff5cdfa3abc8a52" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2505,9 +2504,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "0e63d1795c565ac3462334c1e396fd46dbf481c40f51f5072c310717bc4fb309" dependencies = [ "bumpalo", "proc-macro2", @@ -2518,18 +2517,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "e9f9cdac23a5ce71f6bf9f8824898a501e511892791ea2a0c6b8568c68b9cb53" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.85" +version = "0.3.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +checksum = "f2c7c5718134e770ee62af3b6b4a84518ec10101aad610c024b64d6ff29bb1ff" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/src/analysis.rs b/src/analysis.rs new file mode 100644 index 0000000..093e232 --- /dev/null +++ b/src/analysis.rs @@ -0,0 +1,132 @@ +use anyhow::{Context, Result}; +use pulldown_cmark::{Event, Parser, Tag}; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +use crate::WikiConfig; + +pub struct WikiGraph { + pub nodes: HashMap, + pub edges: HashMap>, +} + +impl WikiGraph { + pub async fn new(docs_dir: PathBuf) -> Result { + let mut nodes = HashMap::new(); + let mut raw_files = HashMap::new(); + + // Scan for all TOML files + let mut entries = tokio::fs::read_dir(&docs_dir).await?; + while let Some(entry) = entries.next_entry().await? { + let path = entry.path(); + if path.extension().and_then(|s| s.to_str()) == Some("toml") { + let content = tokio::fs::read_to_string(&path).await?; + let config: WikiConfig = toml::from_str(&content) + .with_context(|| format!("Failed to parse {:?}", path))?; + + let slug = path + .file_stem() + .and_then(|s| s.to_str()) + .unwrap() + .to_string(); + + nodes.insert(slug.clone(), config.clone()); + raw_files.insert(slug, config); + } + } + + // Scan content for links + let mut edges = HashMap::new(); + for (slug, config) in &raw_files { + let mut page_links = Vec::new(); + + if let Some(content_file) = &config.content_file { + let md_path = docs_dir.join(content_file); + if md_path.exists() { + let md_content = tokio::fs::read_to_string(&md_path).await?; + let links = extract_markdown_links(&md_content); + + for link in links { + // Normalize link + let target_slug = normalize_link(&link); + // Only add if not a self-link + if target_slug != *slug { + page_links.push(target_slug); + } + } + } + } + edges.insert(slug.clone(), page_links); + } + + Ok(Self { nodes, edges }) + } + + pub fn print_dot(&self) { + println!("digraph Wiki {{"); + println!(" graph [layout=neato, overlap=false, splines=true];"); + println!(" node [shape=box, style=\"filled,rounded\", fontname=\"Helvetica\"];"); + + // Nodes + for (slug, config) in &self.nodes { + println!( + " \"{}\" [label=\"{}\", href=\"{}.html\"];", + slug, config.title, slug + ); + } + + println!(""); + + // Edges + for (source, targets) in &self.edges { + for target in targets { + if self.nodes.contains_key(target) { + println!(" \"{}\" -> \"{}\";", source, target); + } + } + } + + println!("}}"); + } + + pub fn check_dead_links(&self) { + let mut found_issues = false; + + for (source, targets) in &self.edges { + for target in targets { + if !self.nodes.contains_key(target) { + println!("\x1b[1m{}\x1b[0m -> ❌ \x1b[31m{}\x1b[0m", source, target); + found_issues = true; + } + } + } + + if !found_issues { + println!("✅ No broken links found!"); + } + } +} + +fn extract_markdown_links(content: &str) -> Vec { + let parser = Parser::new(content); + let mut links = Vec::new(); + + for event in parser { + if let Event::Start(Tag::Link { dest_url, .. }) = event { + let url = dest_url.to_string(); + // Filter out external links + if !url.starts_with("http") && !url.starts_with("mailto:") && !url.starts_with('#') { + links.push(url); + } + } + } + links +} + +fn normalize_link(link: &str) -> String { + let path = Path::new(link); + // Remove extension + let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or(link); + // Remove leading ./ + stem.trim_start_matches("./").to_string() +} diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..4a4a211 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,44 @@ +use std::path::PathBuf; + +use clap::{Parser, Subcommand}; + +#[derive(Parser)] +#[command(author, version, about = "A simple wiki server/builder")] +pub struct Cli { + #[command(subcommand)] + pub command: Commands, +} + +#[derive(Subcommand)] +pub enum Commands { + /// Serve the wiki locally + Serve { + #[arg(short, long)] + path: PathBuf, + #[arg(short, long)] + no_navigation: bool, + #[arg(short = 'P', long, default_value = "8090")] + port: u16, + #[arg(short = 'H', long)] + host: bool, + }, + /// Build the static site + Build { + #[arg(short, long)] + path: PathBuf, + #[arg(short, long)] + no_navigation: bool, + #[arg(short, long)] + out_dir: Option, + }, + /// Output a DOT graph of the wiki connections + Graph { + #[arg(short, long)] + path: PathBuf, + }, + /// List broken links + Todo { + #[arg(short, long)] + path: PathBuf, + }, +} diff --git a/src/main.rs b/src/main.rs index b2940d9..2505912 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,21 +1,27 @@ -use ax_models::{Page, WikiConfig}; use axum::{ Router, - extract::{Path, State}, http::StatusCode, - response::{Html, IntoResponse, Response}, + response::{IntoResponse, Response}, routing::get, }; -use clap::{Parser, Subcommand}; +use clap::Parser; use lazy_static::lazy_static; -use pulldown_cmark::{Options, Parser as MarkdownParser, html}; use std::sync::Arc; use std::{io::Cursor, path::PathBuf}; use syntect::{highlighting::ThemeSet, parsing::SyntaxSet}; use tera::{Context, Tera}; +mod analysis; +mod cli; mod codeblocks; -use codeblocks::*; +mod rendering; + +use crate::{ + cli::{Cli, Commands}, + rendering::{render_page_handler, render_summary_handler, render_wiki_page}, +}; +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; lazy_static! { pub static ref TEMPLATES: Tera = { @@ -46,33 +52,19 @@ lazy_static! { }; } -#[derive(Parser)] -#[command(author, version, about = "A simple wiki server/builder")] -struct Cli { - #[command(subcommand)] - command: Commands, +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct Page { + pub filename: String, + pub title: String, + pub datetime: String, } -#[derive(Subcommand)] -enum Commands { - Serve { - #[arg(short, long)] - path: PathBuf, - #[arg(short, long)] - no_navigation: bool, - #[arg(short = 'P', long, default_value = "8090")] - port: u16, - #[arg(short = 'H', long)] - host: bool, - }, - Build { - #[arg(short, long)] - path: PathBuf, - #[arg(short, long)] - no_navigation: bool, - #[arg(short, long)] - out_dir: Option, - }, +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct WikiConfig { + pub title: String, + pub image: Option, + pub infobox: Option>, + pub content_file: Option, } struct AppState { @@ -105,7 +97,6 @@ async fn main() -> anyhow::Result<()> { .route("/", get(render_summary_handler)) .route("/{page}", get(render_page_handler)) .route("/style.css", get(serve_css)) - // Serve images relative to the docs directory .nest_service( "/assets", tower_http::services::ServeDir::new(&shared_state.docs_dir), @@ -131,6 +122,16 @@ async fn main() -> anyhow::Result<()> { tokio::fs::create_dir_all(&output_path).await?; run_build(abs_path, output_path, no_navigation).await?; } + Commands::Graph { path } => { + let abs_path = std::fs::canonicalize(&path)?; + let graph = analysis::WikiGraph::new(abs_path).await?; + graph.print_dot(); + } + Commands::Todo { path } => { + let abs_path = std::fs::canonicalize(&path)?; + let graph = analysis::WikiGraph::new(abs_path).await?; + graph.check_dead_links(); + } } Ok(()) } @@ -140,7 +141,6 @@ async fn get_summary_data(docs_dir: &PathBuf) -> Vec { if let Ok(mut entries) = tokio::fs::read_dir(docs_dir).await { while let Ok(Some(entry)) = entries.next_entry().await { let path = entry.path(); - // We now look for TOML files as the entry points if path.extension().and_then(|s| s.to_str()) != Some("toml") { continue; } @@ -148,7 +148,6 @@ async fn get_summary_data(docs_dir: &PathBuf) -> Vec { let filename = entry.file_name(); let filename_str = filename.to_str().unwrap_or(""); - // Read TOML to get the title let title = if let Ok(content) = tokio::fs::read_to_string(&path).await { if let Ok(config) = toml::from_str::(&content) { config.title @@ -159,13 +158,10 @@ async fn get_summary_data(docs_dir: &PathBuf) -> Vec { filename_str.to_string() }; - // TODO: - let datetime = "".to_string(); - pages.push(Page { - filename: filename_str.to_string(), // Keep .toml extension here for now + filename: filename_str.to_string(), title, - datetime, + datetime: "".to_string(), }); } } @@ -173,89 +169,9 @@ async fn get_summary_data(docs_dir: &PathBuf) -> Vec { pages } -async fn render_wiki_page( - filename: &str, - docs_dir: &PathBuf, - no_navigation: bool, - is_static: bool, -) -> Result { - let toml_path = docs_dir.join(filename); - let toml_content = tokio::fs::read_to_string(&toml_path) - .await - .map_err(|_| "Page configuration not found".to_string())?; - - let config: WikiConfig = - toml::from_str(&toml_content).map_err(|e| format!("Invalid TOML configuration: {}", e))?; - - let markdown_content = if let Some(md_file) = &config.content_file { - let md_path = docs_dir.join(md_file); - tokio::fs::read_to_string(&md_path) - .await - .unwrap_or_else(|_| { - "# Content missing\nThe linked markdown file could not be found.".to_string() - }) - } else { - String::new() - }; - - // Render Markdown - let mut options = Options::empty(); - options.insert( - Options::ENABLE_TABLES - | Options::ENABLE_FOOTNOTES - | Options::ENABLE_STRIKETHROUGH - | Options::ENABLE_TASKLISTS, - ); - - let parser = MarkdownParser::new_ext(&markdown_content, options); - let renderer = CodeblockRenderer::new(parser); - let mut html_output = String::new(); - html::push_html(&mut html_output, renderer); - - let (mut prev, mut next) = if no_navigation { - (None, None) - } else { - get_nav_links(docs_dir, filename) - }; - - if is_static { - prev = prev.map(|s| { - if s == "." { - "index.html".to_string() - } else { - s.replace(".toml", ".html") - } - }); - next = next.map(|s| s.replace(".toml", ".html")); - } - - let infobox_list: Vec = match config.infobox { - Some(map) => map - .into_iter() - .map(|(k, v)| InfoboxItem { key: k, value: v }) - .collect(), - None => Vec::new(), - }; - - let mut context = Context::new(); - context.insert("title", &config.title); - context.insert("content", &html_output); - context.insert("infobox", &infobox_list); // Pass the ordered list, not the map - context.insert("main_image", &config.image); - context.insert("prev_page", &prev); - context.insert("next_page", &next); - context.insert("no_navigation", &no_navigation); - context.insert("is_static", &is_static); - - TEMPLATES - .render("page.html", &context) - .map_err(|e| format!("Template Error: {}", e)) -} - async fn run_build(docs_dir: PathBuf, out_dir: PathBuf, no_navigation: bool) -> anyhow::Result<()> { tracing::info!("Building static site to: {:?}", out_dir); - // Build summary if !no_navigation { let pages = get_summary_data(&docs_dir).await; let static_pages: Vec = pages @@ -275,13 +191,9 @@ async fn run_build(docs_dir: PathBuf, out_dir: PathBuf, no_navigation: bool) -> tokio::fs::write(out_dir.join("index.html"), rendered).await?; } - // Build css let css = TEMPLATES.render("style.css", &Context::new())?; tokio::fs::write(out_dir.join("style.css"), css).await?; - // Copy assets (images, etc) - // NOTE: In a real app you'd recursively copy everything not .md/.toml - // For now we just copy files that look like images if they are in root let mut entries = tokio::fs::read_dir(&docs_dir).await?; while let Some(entry) = entries.next_entry().await? { let path = entry.path(); @@ -293,7 +205,6 @@ async fn run_build(docs_dir: PathBuf, out_dir: PathBuf, no_navigation: bool) -> } } - // Build pages let mut entries = tokio::fs::read_dir(&docs_dir).await?; while let Some(entry) = entries.next_entry().await? { let path = entry.path(); @@ -315,38 +226,6 @@ async fn run_build(docs_dir: PathBuf, out_dir: PathBuf, no_navigation: bool) -> Ok(()) } -async fn render_summary_handler(State(state): State>) -> impl IntoResponse { - if state.no_navigation { - return (StatusCode::NOT_FOUND, "Disabled").into_response(); - } - let pages = get_summary_data(&state.docs_dir).await; - let mut context = Context::new(); - context.insert("title", "Wiki Index"); - context.insert("files", &pages); - context.insert("is_static", &false); - - match TEMPLATES.render("home.html", &context) { - Ok(rendered) => Html(rendered).into_response(), - Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), - } -} - -async fn render_page_handler( - State(state): State>, - Path(page): Path, -) -> impl IntoResponse { - let filename = if page.ends_with(".toml") { - page - } else { - format!("{}.toml", page) - }; - - match render_wiki_page(&filename, &state.docs_dir, state.no_navigation, false).await { - Ok(html) => Html(html).into_response(), - Err(e) => (StatusCode::NOT_FOUND, format!("

404

{}

", e)).into_response(), - } -} - async fn serve_css() -> impl IntoResponse { match TEMPLATES.render("style.css", &Context::new()) { Ok(css) => Response::builder() @@ -357,34 +236,6 @@ async fn serve_css() -> impl IntoResponse { } } -mod ax_models { - use indexmap::IndexMap; - use serde::{Deserialize, Serialize}; // Use IndexMap instead of BTreeMap - - #[derive(Deserialize, Serialize, Clone)] - pub struct Page { - pub filename: String, - pub title: String, - pub datetime: String, - } - - #[derive(Deserialize, Serialize, Clone)] - pub struct WikiConfig { - pub title: String, - pub image: Option, - // IndexMap preserves the order from the file - pub infobox: Option>, - pub content_file: Option, - } -} - -// Helper struct for the template -#[derive(serde::Serialize)] -struct InfoboxItem { - key: String, - value: String, -} - fn get_nav_links(dir: &PathBuf, current_file: &str) -> (Option, Option) { let mut files: Vec = std::fs::read_dir(dir) .unwrap() diff --git a/src/rendering.rs b/src/rendering.rs new file mode 100644 index 0000000..433a6d8 --- /dev/null +++ b/src/rendering.rs @@ -0,0 +1,129 @@ +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::{Html, IntoResponse}, +}; +use pulldown_cmark::{Options, Parser as MarkdownParser, html}; +use std::path::PathBuf; +use std::sync::Arc; +use tera::Context; + +use crate::{ + AppState, TEMPLATES, WikiConfig, codeblocks::CodeblockRenderer, get_nav_links, get_summary_data, +}; + +#[derive(serde::Serialize)] +struct InfoboxItem { + key: String, + value: String, +} + +pub async fn render_wiki_page( + filename: &str, + docs_dir: &PathBuf, + no_navigation: bool, + is_static: bool, +) -> Result { + let toml_path = docs_dir.join(filename); + let toml_content = tokio::fs::read_to_string(&toml_path) + .await + .map_err(|_| "Page configuration not found".to_string())?; + + let config: WikiConfig = + toml::from_str(&toml_content).map_err(|e| format!("Invalid TOML configuration: {}", e))?; + + let markdown_content = if let Some(md_file) = &config.content_file { + let md_path = docs_dir.join(md_file); + tokio::fs::read_to_string(&md_path) + .await + .unwrap_or_else(|_| { + "# Content missing\nThe linked markdown file could not be found.".to_string() + }) + } else { + String::new() + }; + + let mut options = Options::empty(); + options.insert( + Options::ENABLE_TABLES + | Options::ENABLE_FOOTNOTES + | Options::ENABLE_STRIKETHROUGH + | Options::ENABLE_TASKLISTS, + ); + + let parser = MarkdownParser::new_ext(&markdown_content, options); + let renderer = CodeblockRenderer::new(parser); + let mut html_output = String::new(); + html::push_html(&mut html_output, renderer); + + let (mut prev, mut next) = if no_navigation { + (None, None) + } else { + get_nav_links(docs_dir, filename) + }; + + if is_static { + prev = prev.map(|s| { + if s == "." { + "index.html".to_string() + } else { + s.replace(".toml", ".html") + } + }); + next = next.map(|s| s.replace(".toml", ".html")); + } + + let infobox_list: Vec = match config.infobox { + Some(map) => map + .into_iter() + .map(|(k, v)| InfoboxItem { key: k, value: v }) + .collect(), + None => Vec::new(), + }; + + let mut context = Context::new(); + context.insert("title", &config.title); + context.insert("content", &html_output); + context.insert("infobox", &infobox_list); + context.insert("main_image", &config.image); + context.insert("prev_page", &prev); + context.insert("next_page", &next); + context.insert("no_navigation", &no_navigation); + context.insert("is_static", &is_static); + + TEMPLATES + .render("page.html", &context) + .map_err(|e| format!("Template Error: {}", e)) +} + +pub async fn render_summary_handler(State(state): State>) -> impl IntoResponse { + if state.no_navigation { + return (StatusCode::NOT_FOUND, "Disabled").into_response(); + } + let pages = get_summary_data(&state.docs_dir).await; + let mut context = Context::new(); + context.insert("title", "Wiki Index"); + context.insert("files", &pages); + context.insert("is_static", &false); + + match TEMPLATES.render("home.html", &context) { + Ok(rendered) => Html(rendered).into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), + } +} + +pub async fn render_page_handler( + State(state): State>, + Path(page): Path, +) -> impl IntoResponse { + let filename = if page.ends_with(".toml") { + page + } else { + format!("{}.toml", page) + }; + + match render_wiki_page(&filename, &state.docs_dir, state.no_navigation, false).await { + Ok(html) => Html(html).into_response(), + Err(e) => (StatusCode::NOT_FOUND, format!("

404

{}

", e)).into_response(), + } +}