From e9855ec6a19530c03ad853568eabd98541f2f9aa Mon Sep 17 00:00:00 2001 From: eiiko6 Date: Sat, 28 Feb 2026 19:09:53 +0100 Subject: [PATCH] added categories --- example/azrak.md | 31 ------------- example/azrak.toml | 11 ----- example/bernardo.md | 4 ++ example/bernardo.toml | 1 + example/lumina.toml | 1 + example/sword-of-conjuring.md | 3 ++ example/sword-of-conjuring.toml | 9 ++++ src/cli.rs | 3 ++ src/entry.rs | 15 ++++-- src/fs.rs | 82 +++++++++++++++++++++++++++++++++ src/main.rs | 73 ++--------------------------- src/rendering.rs | 5 +- templates/home.html | 37 ++++++++++----- templates/page.html | 7 ++- templates/style.css | 16 +++++++ 15 files changed, 169 insertions(+), 129 deletions(-) delete mode 100644 example/azrak.md delete mode 100644 example/azrak.toml create mode 100644 example/sword-of-conjuring.md create mode 100644 example/sword-of-conjuring.toml create mode 100644 src/fs.rs diff --git a/example/azrak.md b/example/azrak.md deleted file mode 100644 index 672bb8b..0000000 --- a/example/azrak.md +++ /dev/null @@ -1,31 +0,0 @@ -# Azrak the Wall-Eater - -Azrak does not eat food. - -Azrak eats **structures**. - -Concrete. -Drywall. -The concept of shelter. - ---- - -## Lore - -When humans invented houses, Azrak woke up. - -Every abandoned building is believed to be a corpse. - ---- - -## Family - -- Mother: [Voidmother](voidmother) -- Sister: [Lumina](lumina) - ---- - -## Fun Fact - -Azrak once ate half a shopping mall because a light flickered funny. - diff --git a/example/azrak.toml b/example/azrak.toml deleted file mode 100644 index 2c2b9e0..0000000 --- a/example/azrak.toml +++ /dev/null @@ -1,11 +0,0 @@ -title = "Azrak the Wall-Eater" -image = "azrak.png" -content_file = "azrak.md" - -[infobox] -"Espèce" = "Devourer Papoute" -"Genre" = "M" -"Domain" = "Hunger & Destruction" -"Enemy of" = "Entire buildings" -"Mother" = "Voidmother" - diff --git a/example/bernardo.md b/example/bernardo.md index 482b37c..c77f19c 100644 --- a/example/bernardo.md +++ b/example/bernardo.md @@ -3,3 +3,7 @@ Bernardo est un Papoute commun. Il est souvent considéré en tant qu'example de ## Répliques > ... + +## Belongings + +Some say Bernardo owns the [Sword of Conjuring](sword-of-conjuring). diff --git a/example/bernardo.toml b/example/bernardo.toml index 4c158a2..4972ae0 100644 --- a/example/bernardo.toml +++ b/example/bernardo.toml @@ -1,4 +1,5 @@ title = "Bernardo" +category = "Characters" image = "bernardo.png" content_file = "bernardo.md" diff --git a/example/lumina.toml b/example/lumina.toml index b566fe7..c0553ef 100644 --- a/example/lumina.toml +++ b/example/lumina.toml @@ -1,4 +1,5 @@ title = "Lumina the Warm One" +category = "Characters" image = "lumina.png" content_file = "lumina.md" diff --git a/example/sword-of-conjuring.md b/example/sword-of-conjuring.md new file mode 100644 index 0000000..2248191 --- /dev/null +++ b/example/sword-of-conjuring.md @@ -0,0 +1,3 @@ +# Sword of Conjuring + +This is a sword what do you expect diff --git a/example/sword-of-conjuring.toml b/example/sword-of-conjuring.toml new file mode 100644 index 0000000..00033d2 --- /dev/null +++ b/example/sword-of-conjuring.toml @@ -0,0 +1,9 @@ +title = "Sword of Conjuring" +category = "Items" +image = "azrak.png" +content_file = "azrak.md" + +[infobox] +"Weapon type" = "Sword" +"Material" = "Steel" + diff --git a/src/cli.rs b/src/cli.rs index 5560336..878a54c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -60,6 +60,9 @@ pub enum EntryCommands { New { /// The title of the new entry (e.g. "The Great Bernardo") name: String, + /// Optional category/type for the entry + #[arg(short, long)] + category: Option, }, /// Remove an entry by its normalized name Remove { diff --git a/src/entry.rs b/src/entry.rs index 73673b1..306503e 100644 --- a/src/entry.rs +++ b/src/entry.rs @@ -9,7 +9,7 @@ use crate::cli::EntryCommands; pub async fn handle(command: EntryCommands, root_dir: PathBuf) -> Result<()> { match command { EntryCommands::List => list_entries(&root_dir).await, - EntryCommands::New { name } => create_entry(&root_dir, &name).await, + EntryCommands::New { name, category } => create_entry(&root_dir, &name, category).await, EntryCommands::Remove { name } => remove_entry(&root_dir, &name).await, EntryCommands::Inspect { name } => inspect_entry(&root_dir, &name).await, } @@ -73,7 +73,7 @@ fn check_file_status(root: &PathBuf, filename: Option) -> String { } } -async fn create_entry(root: &PathBuf, title: &str) -> Result<()> { +async fn create_entry(root: &PathBuf, title: &str, category: Option) -> Result<()> { let slug: String = title .trim() .to_lowercase() @@ -102,7 +102,7 @@ async fn create_entry(root: &PathBuf, title: &str) -> Result<()> { // Minimal default content let toml_content = format!( - r#"title = "{}" + r#"title = "{}"{} image = "{}" content_file = "{}" @@ -111,7 +111,14 @@ content_file = "{}" "Status" = "WIP" "Created" = "2026" "Category" = "General""#, - title, img_filename, md_filename + title, + if let Some(cat) = category { + format!("\ncategory = \"{cat}\"") + } else { + String::new() + }, + img_filename, + md_filename ); fs::write(&toml_path, toml_content) diff --git a/src/fs.rs b/src/fs.rs new file mode 100644 index 0000000..a83ebf9 --- /dev/null +++ b/src/fs.rs @@ -0,0 +1,82 @@ +use std::path::PathBuf; + +use crate::{Page, WikiConfig}; + +pub async fn get_summary_data(docs_dir: &PathBuf, is_static: bool) -> Vec { + let mut pages = Vec::new(); + 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(); + if path.extension().and_then(|s| s.to_str()) != Some("toml") + || path.file_name().and_then(|s| s.to_str()) == Some("_changelog.toml") + { + continue; + } + + let filename_str = if is_static { + entry.file_name().to_string_lossy().into_owned() + } else { + path.file_stem() + .map(|s| s.to_string_lossy().into_owned()) + .unwrap_or_else(|| entry.file_name().to_string_lossy().into_owned()) + }; + + let (title, category) = if let Ok(content) = tokio::fs::read_to_string(&path).await { + if let Ok(config) = toml::from_str::(&content) { + (config.title, config.category) + } else { + (filename_str.clone(), None) + } + } else { + (filename_str.clone(), None) + }; + + pages.push(Page { + filename: filename_str, + title, + category, + datetime: "".to_string(), + }); + } + } + + pages.sort_by(|a, b| match (&a.category, &b.category) { + (None, Some(_)) => std::cmp::Ordering::Less, + (Some(_), None) => std::cmp::Ordering::Greater, + (Some(c1), Some(c2)) => match c1.cmp(c2) { + std::cmp::Ordering::Equal => a.title.cmp(&b.title), + ord => ord, + }, + (None, None) => a.title.cmp(&b.title), + }); + pages +} + +pub fn get_nav_links(dir: &PathBuf, current_file: &str) -> (Option, Option) { + let mut files: Vec = std::fs::read_dir(dir) + .unwrap() + .filter_map(|entry| { + let path = entry.ok()?.path(); + if path.extension()? == "toml" { + Some(path.file_name()?.to_str()?.to_string()) + } else { + None + } + }) + .collect(); + + files.sort(); + let pos = files.iter().position(|f| f == current_file); + match pos { + Some(i) => { + let prev = if i == 0 { + Some(".".to_string()) + } else { + files.get(i - 1).cloned() + }; + let next = files.get(i + 1).cloned(); + (prev, next) + } + None => (None, None), + } +} diff --git a/src/main.rs b/src/main.rs index 58e5cb4..1715c1a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,10 +15,12 @@ mod analysis; mod changelog; mod cli; mod entry; +mod fs; mod rendering; use crate::{ cli::{Cli, Commands}, + fs::get_summary_data, rendering::{ render_changelog_handler, render_page_handler, render_summary_handler, render_wiki_page, }, @@ -64,11 +66,13 @@ pub struct Page { pub filename: String, pub title: String, pub datetime: String, + pub category: Option, } #[derive(Deserialize, Serialize, Clone, Debug)] pub struct WikiConfig { pub title: String, + pub category: Option, pub image: Option, pub infobox: Option>, pub content_file: Option, @@ -154,46 +158,6 @@ async fn main() -> anyhow::Result<()> { Ok(()) } -async fn get_summary_data(docs_dir: &PathBuf, is_static: bool) -> Vec { - let mut pages = Vec::new(); - 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(); - if path.extension().and_then(|s| s.to_str()) != Some("toml") - || path.file_name().and_then(|s| s.to_str()) == Some("_changelog.toml") - { - continue; - } - - let filename_str = if is_static { - entry.file_name().to_string_lossy().into_owned() - } else { - path.file_stem() - .map(|s| s.to_string_lossy().into_owned()) - .unwrap_or_else(|| entry.file_name().to_string_lossy().into_owned()) - }; - - let title = if let Ok(content) = tokio::fs::read_to_string(&path).await { - if let Ok(config) = toml::from_str::(&content) { - config.title - } else { - filename_str.clone() - } - } else { - filename_str.clone() - }; - - pages.push(Page { - filename: filename_str, - title, - datetime: "".to_string(), - }); - } - } - pages.sort_by(|a, b| a.title.cmp(&b.title)); - pages -} - async fn run_build(docs_dir: PathBuf, out_dir: PathBuf, no_navigation: bool) -> anyhow::Result<()> { tracing::info!("Building static site to: {}", out_dir.display()); @@ -287,32 +251,3 @@ async fn serve_css() -> impl IntoResponse { Err(_) => (StatusCode::NOT_FOUND, "CSS not found").into_response(), } } - -fn get_nav_links(dir: &PathBuf, current_file: &str) -> (Option, Option) { - let mut files: Vec = std::fs::read_dir(dir) - .unwrap() - .filter_map(|entry| { - let path = entry.ok()?.path(); - if path.extension()? == "toml" { - Some(path.file_name()?.to_str()?.to_string()) - } else { - None - } - }) - .collect(); - - files.sort(); - let pos = files.iter().position(|f| f == current_file); - match pos { - Some(i) => { - let prev = if i == 0 { - Some(".".to_string()) - } else { - files.get(i - 1).cloned() - }; - let next = files.get(i + 1).cloned(); - (prev, next) - } - None => (None, None), - } -} diff --git a/src/rendering.rs b/src/rendering.rs index b53a4ca..eb34ab3 100644 --- a/src/rendering.rs +++ b/src/rendering.rs @@ -13,8 +13,8 @@ use syntect::html::highlighted_html_for_string; use tera::Context; use crate::{ - AppState, SYNTAX_SET, TEMPLATES, THEME_SET, WikiConfig, changelog, get_nav_links, - get_summary_data, + AppState, SYNTAX_SET, TEMPLATES, THEME_SET, WikiConfig, changelog, + fs::{get_nav_links, get_summary_data}, }; pub struct Renderer<'a> { @@ -186,6 +186,7 @@ pub async fn render_wiki_page( let mut context = Context::new(); context.insert("title", &config.title); + context.insert("category", &config.category); context.insert("content", &html_output); context.insert("infobox", &infobox_list); context.insert("main_image", &main_image_path); diff --git a/templates/home.html b/templates/home.html index c2cb616..489e686 100644 --- a/templates/home.html +++ b/templates/home.html @@ -3,18 +3,33 @@ {% block content %}

{{ title }}

-
    - {% for file in files %} -
  1. - {{ file.title }} - - - {{ file.datetime }} - -
  2. - {% endfor %} -
+
+ {% set last_category = "___INIT___" %} -
+ {% for file in files %} + {% set current_cat = file.category | default(value="Uncategorized") %} + + {# If file.category is None, we treat it as special top-level items #} + {% if not file.category %} + {# Just print the item, assuming sorted at top #} +
+ {{ file.title }} + {{ file.datetime }} +
+ {% else %} + {# If it is a categorized item #} + {% if current_cat != last_category %} +

{{ current_cat }}

+ {% set_global last_category = current_cat %} + {% endif %} + +
+ {{ file.title }} + {{ file.datetime }} +
+ {% endif %} + {% endfor %} +

Recent Changes

{% if changelog | length > 0 %} diff --git a/templates/page.html b/templates/page.html index 126390a..2b3c31c 100644 --- a/templates/page.html +++ b/templates/page.html @@ -2,7 +2,12 @@ {% block title %}{{ title }}{% endblock title %} {% block content %}
-

{{ title }}

+

+ {{ title }} + {% if category %} + #{{ category }} + {% endif %} +

diff --git a/templates/style.css b/templates/style.css index 54827b3..d6d3d16 100644 --- a/templates/style.css +++ b/templates/style.css @@ -241,3 +241,19 @@ nav a { font-weight: bold; font-size: 1.2rem; color: var(--text-main); } font-family: 'JetBrains Mono', monospace; } + +.header-category { + font-size: 1.2rem; + color: var(--text-muted); + font-weight: normal; + margin-left: 15px; + vertical-align: middle; + opacity: 0.7; +} + +.category-header { + margin-top: 2rem; + margin-bottom: 0.5rem; + border-bottom: 1px solid var(--border-color); + padding-bottom: 0.2rem; +}