added the entry command
This commit is contained in:
10
Cargo.lock
generated
10
Cargo.lock
generated
@@ -340,6 +340,15 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colored"
|
||||||
|
version = "3.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "combine"
|
name = "combine"
|
||||||
version = "4.6.7"
|
version = "4.6.7"
|
||||||
@@ -2561,6 +2570,7 @@ dependencies = [
|
|||||||
"axum",
|
"axum",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
|
"colored",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"pulldown-cmark",
|
"pulldown-cmark",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ tracing = "0.1.44"
|
|||||||
tracing-subscriber = "0.3.22"
|
tracing-subscriber = "0.3.22"
|
||||||
toml = "0.8.19"
|
toml = "0.8.19"
|
||||||
indexmap = { version = "2.7.1", features = ["serde"] }
|
indexmap = { version = "2.7.1", features = ["serde"] }
|
||||||
|
colored = "3.1.1"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
reqwest = { version = "0.13.1", features = ["blocking"] }
|
reqwest = { version = "0.13.1", features = ["blocking"] }
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
use colored::*;
|
||||||
use pulldown_cmark::{Event, Parser, Tag};
|
use pulldown_cmark::{Event, Parser, Tag};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@@ -95,14 +96,14 @@ impl WikiGraph {
|
|||||||
for (source, targets) in &self.edges {
|
for (source, targets) in &self.edges {
|
||||||
for target in targets {
|
for target in targets {
|
||||||
if !self.nodes.contains_key(target) {
|
if !self.nodes.contains_key(target) {
|
||||||
println!("\x1b[1m{}\x1b[0m -> ❌ \x1b[31m{}\x1b[0m", source, target);
|
println!("{} -> ❌ {}", source.bold().cyan(), target.red());
|
||||||
found_issues = true;
|
found_issues = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !found_issues {
|
if !found_issues {
|
||||||
println!("✅ No broken links found!");
|
println!("{}", "✅ No broken links found!".green().bold());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
40
src/cli.rs
40
src/cli.rs
@@ -5,6 +5,8 @@ use clap::{Parser, Subcommand};
|
|||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(author, version, about = "A simple wiki server/builder")]
|
#[command(author, version, about = "A simple wiki server/builder")]
|
||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
|
#[arg(short, long, global = true, default_value = ".")]
|
||||||
|
pub path: PathBuf,
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub command: Commands,
|
pub command: Commands,
|
||||||
}
|
}
|
||||||
@@ -13,8 +15,6 @@ pub struct Cli {
|
|||||||
pub enum Commands {
|
pub enum Commands {
|
||||||
/// Serve the wiki locally
|
/// Serve the wiki locally
|
||||||
Serve {
|
Serve {
|
||||||
#[arg(short, long)]
|
|
||||||
path: PathBuf,
|
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
no_navigation: bool,
|
no_navigation: bool,
|
||||||
#[arg(short = 'P', long, default_value = "8090")]
|
#[arg(short = 'P', long, default_value = "8090")]
|
||||||
@@ -24,21 +24,39 @@ pub enum Commands {
|
|||||||
},
|
},
|
||||||
/// Build the static site
|
/// Build the static site
|
||||||
Build {
|
Build {
|
||||||
#[arg(short, long)]
|
|
||||||
path: PathBuf,
|
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
no_navigation: bool,
|
no_navigation: bool,
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
out_dir: Option<PathBuf>,
|
out_dir: Option<PathBuf>,
|
||||||
},
|
},
|
||||||
/// Output a DOT graph of the wiki connections
|
/// Output a DOT graph of the wiki connections
|
||||||
Graph {
|
Graph {},
|
||||||
#[arg(short, long)]
|
|
||||||
path: PathBuf,
|
|
||||||
},
|
|
||||||
/// List broken links
|
/// List broken links
|
||||||
Todo {
|
Todo {},
|
||||||
#[arg(short, long)]
|
/// Manage wiki entries
|
||||||
path: PathBuf,
|
Entry {
|
||||||
|
#[command(subcommand)]
|
||||||
|
cmd: EntryCommands,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
pub enum EntryCommands {
|
||||||
|
/// List all existing entries
|
||||||
|
List,
|
||||||
|
/// Create a new entry
|
||||||
|
New {
|
||||||
|
/// The title of the new entry (e.g. "The Great Bernardo")
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
/// Remove an entry by its normalized name
|
||||||
|
Remove {
|
||||||
|
/// The normalized name of the toml file (e.g. "the-great-bernardo")
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
/// Inspect the TOML config of an entry
|
||||||
|
Inspect {
|
||||||
|
/// The normalized name of the toml file (e.g. "the-great-bernardo")
|
||||||
|
name: String,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
173
src/entry.rs
Normal file
173
src/entry.rs
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
use anyhow::{Context, Result, anyhow};
|
||||||
|
use colored::Colorize;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tokio::fs;
|
||||||
|
|
||||||
|
use crate::WikiConfig;
|
||||||
|
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::Remove { name } => remove_entry(&root_dir, &name).await,
|
||||||
|
EntryCommands::Inspect { name } => inspect_entry(&root_dir, &name).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_entries(root: &PathBuf) -> Result<()> {
|
||||||
|
let mut entries = fs::read_dir(root).await?;
|
||||||
|
let mut rows = Vec::new();
|
||||||
|
|
||||||
|
// Scan for .toml files
|
||||||
|
while let Some(entry) = entries.next_entry().await? {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.extension().and_then(|s| s.to_str()) == Some("toml") {
|
||||||
|
let name = path.file_stem().unwrap().to_string_lossy().to_string();
|
||||||
|
|
||||||
|
// Read configuration to check for linked files
|
||||||
|
let content = fs::read_to_string(&path).await.unwrap_or_default();
|
||||||
|
let (md_status, img_status) = if let Ok(config) = toml::from_str::<WikiConfig>(&content)
|
||||||
|
{
|
||||||
|
let md = check_file_status(root, config.content_file);
|
||||||
|
let img = check_file_status(root, config.image);
|
||||||
|
(md, img)
|
||||||
|
} else {
|
||||||
|
("Invalid Config".to_string(), "Unknown".to_string())
|
||||||
|
};
|
||||||
|
|
||||||
|
rows.push((name, md_status, img_status));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
|
|
||||||
|
// Print Table
|
||||||
|
println!(
|
||||||
|
"{:<25} {:<25} {:<25}",
|
||||||
|
"Entry Name", "Markdown File", "Image"
|
||||||
|
);
|
||||||
|
|
||||||
|
for (name, md, img) in rows {
|
||||||
|
println!("{:<25} {:<25} {:<25}", name, md, img);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_file_status(root: &PathBuf, filename: Option<String>) -> String {
|
||||||
|
match filename {
|
||||||
|
Some(f) => {
|
||||||
|
if root.join(&f).exists() {
|
||||||
|
f.green().to_string()
|
||||||
|
} else {
|
||||||
|
format!("{} (Missing)", f.red())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => "None".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_entry(root: &PathBuf, title: &str) -> Result<()> {
|
||||||
|
let slug: String = title
|
||||||
|
.trim()
|
||||||
|
.to_lowercase()
|
||||||
|
.replace([' ', '_'], "-")
|
||||||
|
.chars()
|
||||||
|
.filter(|c| c.is_alphanumeric() || *c == '-')
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if slug.is_empty() {
|
||||||
|
return Err(anyhow!("Resulting entry name is empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let toml_filename = format!("{}.toml", slug);
|
||||||
|
let md_filename = format!("{}.md", slug);
|
||||||
|
let img_filename = format!("{}.png", slug);
|
||||||
|
|
||||||
|
let toml_path = root.join(&toml_filename);
|
||||||
|
let md_path = root.join(&md_filename);
|
||||||
|
|
||||||
|
if toml_path.exists() {
|
||||||
|
return Err(anyhow!("Entry '{}' already exists", slug));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minimal default content
|
||||||
|
let toml_content = format!(
|
||||||
|
r#"title = "{}"
|
||||||
|
image = "{}"
|
||||||
|
content_file = "{}"
|
||||||
|
|
||||||
|
[infobox]
|
||||||
|
"Type" = "Draft"
|
||||||
|
"Status" = "WIP"
|
||||||
|
"Created" = "2026"
|
||||||
|
"Category" = "General""#,
|
||||||
|
title, img_filename, md_filename
|
||||||
|
);
|
||||||
|
|
||||||
|
fs::write(&toml_path, toml_content)
|
||||||
|
.await
|
||||||
|
.context("Failed to write TOML")?;
|
||||||
|
|
||||||
|
// Markdown Placeholder
|
||||||
|
let md_content = format!("Auto-generated markdown file for \"{}\"...", title);
|
||||||
|
fs::write(&md_path, md_content)
|
||||||
|
.await
|
||||||
|
.context("Failed to write Markdown")?;
|
||||||
|
|
||||||
|
println!("Created entry '{}'", title);
|
||||||
|
println!(" - TOML: {:?}", toml_path);
|
||||||
|
println!(" - Markdown: {:?}", md_path);
|
||||||
|
println!(" - (Expected image: {:?})", img_filename);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_entry(root: &PathBuf, name: &str) -> Result<()> {
|
||||||
|
let toml_path = root.join(format!("{}.toml", name));
|
||||||
|
|
||||||
|
if !toml_path.exists() {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"Entry '{}' not found (looked at {:?})",
|
||||||
|
name,
|
||||||
|
toml_path
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = fs::read_to_string(&toml_path).await?;
|
||||||
|
let config: WikiConfig = toml::from_str(&content).context("Failed to parse TOML")?;
|
||||||
|
|
||||||
|
// Try to remove linked files
|
||||||
|
if let Some(md_file) = config.content_file {
|
||||||
|
let p = root.join(&md_file);
|
||||||
|
if p.exists() {
|
||||||
|
fs::remove_file(&p).await?;
|
||||||
|
println!("Removed: {}", md_file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(img_file) = config.image {
|
||||||
|
let p = root.join(&img_file);
|
||||||
|
if p.exists() {
|
||||||
|
fs::remove_file(&p).await?;
|
||||||
|
println!("Removed: {}", img_file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::remove_file(&toml_path).await?;
|
||||||
|
println!("Removed: {}.toml", name);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn inspect_entry(root: &PathBuf, name: &str) -> Result<()> {
|
||||||
|
let toml_path = root.join(format!("{}.toml", name));
|
||||||
|
if !toml_path.exists() {
|
||||||
|
return Err(anyhow!("Entry '{}' not found", name));
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = fs::read_to_string(&toml_path).await?;
|
||||||
|
println!("{}", content);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
17
src/main.rs
17
src/main.rs
@@ -14,6 +14,7 @@ use tera::{Context, Tera};
|
|||||||
mod analysis;
|
mod analysis;
|
||||||
mod cli;
|
mod cli;
|
||||||
mod codeblocks;
|
mod codeblocks;
|
||||||
|
mod entry;
|
||||||
mod rendering;
|
mod rendering;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -81,14 +82,15 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
// Resolve the global path argument once
|
||||||
|
let abs_path = std::fs::canonicalize(&cli.path)?;
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Commands::Serve {
|
Commands::Serve {
|
||||||
path,
|
|
||||||
port,
|
port,
|
||||||
host,
|
host,
|
||||||
no_navigation,
|
no_navigation,
|
||||||
} => {
|
} => {
|
||||||
let abs_path = std::fs::canonicalize(&path)?;
|
|
||||||
let shared_state = Arc::new(AppState {
|
let shared_state = Arc::new(AppState {
|
||||||
docs_dir: abs_path,
|
docs_dir: abs_path,
|
||||||
no_navigation,
|
no_navigation,
|
||||||
@@ -113,25 +115,24 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
axum::serve(listener, app).await?;
|
axum::serve(listener, app).await?;
|
||||||
}
|
}
|
||||||
Commands::Build {
|
Commands::Build {
|
||||||
path,
|
|
||||||
no_navigation,
|
no_navigation,
|
||||||
out_dir,
|
out_dir,
|
||||||
} => {
|
} => {
|
||||||
let abs_path = std::fs::canonicalize(&path)?;
|
|
||||||
let output_path = out_dir.unwrap_or_else(|| abs_path.clone());
|
let output_path = out_dir.unwrap_or_else(|| abs_path.clone());
|
||||||
tokio::fs::create_dir_all(&output_path).await?;
|
tokio::fs::create_dir_all(&output_path).await?;
|
||||||
run_build(abs_path, output_path, no_navigation).await?;
|
run_build(abs_path, output_path, no_navigation).await?;
|
||||||
}
|
}
|
||||||
Commands::Graph { path } => {
|
Commands::Graph {} => {
|
||||||
let abs_path = std::fs::canonicalize(&path)?;
|
|
||||||
let graph = analysis::WikiGraph::new(abs_path).await?;
|
let graph = analysis::WikiGraph::new(abs_path).await?;
|
||||||
graph.print_dot();
|
graph.print_dot();
|
||||||
}
|
}
|
||||||
Commands::Todo { path } => {
|
Commands::Todo {} => {
|
||||||
let abs_path = std::fs::canonicalize(&path)?;
|
|
||||||
let graph = analysis::WikiGraph::new(abs_path).await?;
|
let graph = analysis::WikiGraph::new(abs_path).await?;
|
||||||
graph.check_dead_links();
|
graph.check_dead_links();
|
||||||
}
|
}
|
||||||
|
Commands::Entry { cmd } => {
|
||||||
|
entry::handle(cmd, abs_path).await?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user