added categories

This commit is contained in:
2026-02-28 19:09:53 +01:00
parent be47059b32
commit e9855ec6a1
15 changed files with 169 additions and 129 deletions

View File

@@ -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.

View File

@@ -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"

View File

@@ -3,3 +3,7 @@ Bernardo est un Papoute commun. Il est souvent considéré en tant qu'example de
## Répliques ## Répliques
> ... > ...
## Belongings
Some say Bernardo owns the [Sword of Conjuring](sword-of-conjuring).

View File

@@ -1,4 +1,5 @@
title = "Bernardo" title = "Bernardo"
category = "Characters"
image = "bernardo.png" image = "bernardo.png"
content_file = "bernardo.md" content_file = "bernardo.md"

View File

@@ -1,4 +1,5 @@
title = "Lumina the Warm One" title = "Lumina the Warm One"
category = "Characters"
image = "lumina.png" image = "lumina.png"
content_file = "lumina.md" content_file = "lumina.md"

View File

@@ -0,0 +1,3 @@
# Sword of Conjuring
This is a sword what do you expect

View File

@@ -0,0 +1,9 @@
title = "Sword of Conjuring"
category = "Items"
image = "azrak.png"
content_file = "azrak.md"
[infobox]
"Weapon type" = "Sword"
"Material" = "Steel"

View File

@@ -60,6 +60,9 @@ pub enum EntryCommands {
New { New {
/// The title of the new entry (e.g. "The Great Bernardo") /// The title of the new entry (e.g. "The Great Bernardo")
name: String, name: String,
/// Optional category/type for the entry
#[arg(short, long)]
category: Option<String>,
}, },
/// Remove an entry by its normalized name /// Remove an entry by its normalized name
Remove { Remove {

View File

@@ -9,7 +9,7 @@ use crate::cli::EntryCommands;
pub async fn handle(command: EntryCommands, root_dir: PathBuf) -> Result<()> { pub async fn handle(command: EntryCommands, root_dir: PathBuf) -> Result<()> {
match command { match command {
EntryCommands::List => list_entries(&root_dir).await, 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::Remove { name } => remove_entry(&root_dir, &name).await,
EntryCommands::Inspect { name } => inspect_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>) -> String {
} }
} }
async fn create_entry(root: &PathBuf, title: &str) -> Result<()> { async fn create_entry(root: &PathBuf, title: &str, category: Option<String>) -> Result<()> {
let slug: String = title let slug: String = title
.trim() .trim()
.to_lowercase() .to_lowercase()
@@ -102,7 +102,7 @@ async fn create_entry(root: &PathBuf, title: &str) -> Result<()> {
// Minimal default content // Minimal default content
let toml_content = format!( let toml_content = format!(
r#"title = "{}" r#"title = "{}"{}
image = "{}" image = "{}"
content_file = "{}" content_file = "{}"
@@ -111,7 +111,14 @@ content_file = "{}"
"Status" = "WIP" "Status" = "WIP"
"Created" = "2026" "Created" = "2026"
"Category" = "General""#, "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) fs::write(&toml_path, toml_content)

82
src/fs.rs Normal file
View File

@@ -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<Page> {
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::<WikiConfig>(&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<String>, Option<String>) {
let mut files: Vec<String> = 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),
}
}

View File

@@ -15,10 +15,12 @@ mod analysis;
mod changelog; mod changelog;
mod cli; mod cli;
mod entry; mod entry;
mod fs;
mod rendering; mod rendering;
use crate::{ use crate::{
cli::{Cli, Commands}, cli::{Cli, Commands},
fs::get_summary_data,
rendering::{ rendering::{
render_changelog_handler, render_page_handler, render_summary_handler, render_wiki_page, render_changelog_handler, render_page_handler, render_summary_handler, render_wiki_page,
}, },
@@ -64,11 +66,13 @@ pub struct Page {
pub filename: String, pub filename: String,
pub title: String, pub title: String,
pub datetime: String, pub datetime: String,
pub category: Option<String>,
} }
#[derive(Deserialize, Serialize, Clone, Debug)] #[derive(Deserialize, Serialize, Clone, Debug)]
pub struct WikiConfig { pub struct WikiConfig {
pub title: String, pub title: String,
pub category: Option<String>,
pub image: Option<String>, pub image: Option<String>,
pub infobox: Option<IndexMap<String, String>>, pub infobox: Option<IndexMap<String, String>>,
pub content_file: Option<String>, pub content_file: Option<String>,
@@ -154,46 +158,6 @@ async fn main() -> anyhow::Result<()> {
Ok(()) Ok(())
} }
async fn get_summary_data(docs_dir: &PathBuf, is_static: bool) -> Vec<Page> {
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::<WikiConfig>(&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<()> { 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());
@@ -287,32 +251,3 @@ async fn serve_css() -> impl IntoResponse {
Err(_) => (StatusCode::NOT_FOUND, "CSS not found").into_response(), Err(_) => (StatusCode::NOT_FOUND, "CSS not found").into_response(),
} }
} }
fn get_nav_links(dir: &PathBuf, current_file: &str) -> (Option<String>, Option<String>) {
let mut files: Vec<String> = 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),
}
}

View File

@@ -13,8 +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, changelog, get_nav_links, AppState, SYNTAX_SET, TEMPLATES, THEME_SET, WikiConfig, changelog,
get_summary_data, fs::{get_nav_links, get_summary_data},
}; };
pub struct Renderer<'a> { pub struct Renderer<'a> {
@@ -186,6 +186,7 @@ pub async fn render_wiki_page(
let mut context = Context::new(); let mut context = Context::new();
context.insert("title", &config.title); context.insert("title", &config.title);
context.insert("category", &config.category);
context.insert("content", &html_output); context.insert("content", &html_output);
context.insert("infobox", &infobox_list); context.insert("infobox", &infobox_list);
context.insert("main_image", &main_image_path); context.insert("main_image", &main_image_path);

View File

@@ -3,18 +3,33 @@
{% block content %} {% block content %}
<h1>{{ title }}</h1> <h1>{{ title }}</h1>
<ol> <div class="entry-list">
{% for file in files %} {% set last_category = "___INIT___" %}
<li>
<a href="./{{ file.filename }}">{{ file.title }}</a> -
<span class="local-date" data-timestamp="{{ file.datetime }}">
{{ file.datetime }}
</span>
</li>
{% endfor %}
</ol>
<hr /> {% 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 #}
<div class="entry-item">
<a href="./{{ file.filename }}">{{ file.title }}</a>
<span class="local-date" data-timestamp="{{ file.datetime }}">{{ file.datetime }}</span>
</div>
{% else %}
{# If it is a categorized item #}
{% if current_cat != last_category %}
<h3 class="category-header">{{ current_cat }}</h3>
{% set_global last_category = current_cat %}
{% endif %}
<div class="entry-item indent">
<a href="./{{ file.filename }}">{{ file.title }}</a>
<span class="local-date" data-timestamp="{{ file.datetime }}">{{ file.datetime }}</span>
</div>
{% endif %}
{% endfor %}
</div>
<h2>Recent Changes</h2> <h2>Recent Changes</h2>
{% if changelog | length > 0 %} {% if changelog | length > 0 %}

View File

@@ -2,7 +2,12 @@
{% block title %}{{ title }}{% endblock title %} {% block title %}{{ title }}{% endblock title %}
{% block content %} {% block content %}
<div class="wiki-header"> <div class="wiki-header">
<h1>{{ title }}</h1> <h1>
{{ title }}
{% if category %}
<span class="header-category">#{{ category }}</span>
{% endif %}
</h1>
</div> </div>
<div class="wiki-container"> <div class="wiki-container">

View File

@@ -241,3 +241,19 @@ nav a { font-weight: bold; font-size: 1.2rem; color: var(--text-main); }
font-family: 'JetBrains Mono', monospace; 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;
}