added categories
This commit is contained in:
@@ -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.
|
|
||||||
|
|
||||||
@@ -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"
|
|
||||||
|
|
||||||
@@ -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).
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
title = "Bernardo"
|
title = "Bernardo"
|
||||||
|
category = "Characters"
|
||||||
image = "bernardo.png"
|
image = "bernardo.png"
|
||||||
content_file = "bernardo.md"
|
content_file = "bernardo.md"
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
3
example/sword-of-conjuring.md
Normal file
3
example/sword-of-conjuring.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Sword of Conjuring
|
||||||
|
|
||||||
|
This is a sword what do you expect
|
||||||
9
example/sword-of-conjuring.toml
Normal file
9
example/sword-of-conjuring.toml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
title = "Sword of Conjuring"
|
||||||
|
category = "Items"
|
||||||
|
image = "azrak.png"
|
||||||
|
content_file = "azrak.md"
|
||||||
|
|
||||||
|
[infobox]
|
||||||
|
"Weapon type" = "Sword"
|
||||||
|
"Material" = "Steel"
|
||||||
|
|
||||||
@@ -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 {
|
||||||
|
|||||||
15
src/entry.rs
15
src/entry.rs
@@ -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
82
src/fs.rs
Normal 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
73
src/main.rs
73
src/main.rs
@@ -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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user