diff --git a/markdown_backend/src/main.rs b/markdown_backend/src/main.rs index 176d972..b6cfac1 100644 --- a/markdown_backend/src/main.rs +++ b/markdown_backend/src/main.rs @@ -1,8 +1,12 @@ +#[warn(unused_imports)] use clap::{Parser, Subcommand}; mod markdown; use markdown::{get_all_posts, get_post_by_slug, get_posts_by_tag, watch_posts}; use serde_json; use std::fs; +use std::io; +use std::io::Read; // STD AYOOOOOOOOOOOOOO - Tsodin + #[derive(Parser)] #[command(name = "Markdown Backend")] @@ -28,6 +32,17 @@ enum Commands { Watch, /// Show Rust parser statistics Rsparseinfo, + /// Check backend health + Checkhealth, + /// Parse markdown from file or stdin + Parse { + #[arg(long)] + file: Option, + #[arg(long)] + stdin: bool, + #[arg(long)] + ast: bool, + }, } fn main() { @@ -73,5 +88,43 @@ fn main() { Commands::Rsparseinfo => { println!("{}", markdown::rsparseinfo()); } + Commands::Checkhealth => { + let health = markdown::checkhealth(); + println!("{}", serde_json::to_string_pretty(&health).unwrap()); + } + Commands::Parse { file, stdin, ast } => { + let input = if let Some(file_path) = file { + match std::fs::read_to_string(file_path) { + Ok(content) => content, + Err(e) => { + eprintln!("Failed to read file: {}", e); + std::process::exit(1); + } + } + } else if *stdin { + let mut buffer = String::new(); + if let Err(e) = io::stdin().read_to_string(&mut buffer) { + eprintln!("Failed to read from stdin: {}", e); + std::process::exit(1); + } + buffer + } else { + eprintln!("Please provide --file or --stdin"); + std::process::exit(1); + }; + if *ast { + // Print pulldown_cmark events as debug output + let parser = pulldown_cmark::Parser::new_ext(&input, pulldown_cmark::Options::all()); + for event in parser { + println!("{:?}", event); + } + } else { + // Print HTML output + let parser = pulldown_cmark::Parser::new_ext(&input, pulldown_cmark::Options::all()); + let mut html_output = String::new(); + pulldown_cmark::html::push_html(&mut html_output, parser); + println!("{}", html_output); + } + } } } \ No newline at end of file diff --git a/markdown_backend/src/markdown.rs b/markdown_backend/src/markdown.rs index db93bcf..2d3b6d4 100644 --- a/markdown_backend/src/markdown.rs +++ b/markdown_backend/src/markdown.rs @@ -8,7 +8,7 @@ BLAZINGLY FAST! */ - +#[warn(unused_imports)] use std::fs; use std::path::{Path, PathBuf}; use chrono::{DateTime, Utc}; @@ -29,6 +29,8 @@ use std::collections::HashMap; use std::sync::RwLock; use serde_json; use sysinfo::{System, Pid, RefreshKind, CpuRefreshKind, ProcessRefreshKind}; +use serde::Serialize; +use regex::Regex; const POSTS_CACHE_PATH: &str = "./cache/posts_cache.json"; const POST_STATS_PATH: &str = "./cache/post_stats.json"; @@ -68,6 +70,19 @@ static POST_CACHE: Lazy>> = Lazy::new(|| RwLock::ne static ALL_POSTS_CACHE: Lazy>>> = Lazy::new(|| RwLock::new(None)); static POST_STATS: Lazy>> = Lazy::new(|| RwLock::new(HashMap::new())); +#[derive(Debug, Serialize)] +pub struct HealthReport { + pub posts_dir_exists: bool, + pub posts_count: usize, + pub cache_file_exists: bool, + pub cache_stats_file_exists: bool, + pub cache_readable: bool, + pub cache_stats_readable: bool, + pub cache_post_count: Option, + pub cache_stats_count: Option, + pub errors: Vec, +} + fn get_posts_directory() -> PathBuf { let candidates = [ "./posts", @@ -131,6 +146,51 @@ fn strip_emojis(s: &str) -> String { .collect() } +// Function to process custom tags in markdown content +fn process_custom_tags(content: &str) -> String { + let mut processed = content.to_string(); + + // Handle simple tags without parameters FIRST + let simple_tags = [ + ("", "
This is my custom tag content!
"), + ("", "
⚠️ Warning: This is a custom warning tag!
"), + ("", "
ℹ️ Info: This is a custom info tag!
"), + ("", "
✅ Success: This is a custom success tag!
"), + ("", "
❌ Error: This is a custom error tag!
"), + ]; + + for (tag, replacement) in simple_tags.iter() { + processed = processed.replace(tag, replacement); + } + + // Handle tags with parameters like + let tag_with_params = Regex::new(r"<(\w+)\s+([^>]*?[a-zA-Z0-9=])[^>]*/>").unwrap(); + processed = tag_with_params.replace_all(&processed, |caps: ®ex::Captures| { + let tag_name = &caps[1]; + let params = &caps[2]; + + match tag_name { + "mytag" => { + // Parse parameters and generate custom HTML + format!("
Custom content with params: {}
", params, params) + }, + "alert" => { + // Parse alert type from params + if params.contains("type=\"warning\"") { + "
⚠️ Warning Alert!
".to_string() + } else if params.contains("type=\"error\"") { + "
❌ Error Alert!
".to_string() + } else { + "
ℹ️ Info Alert!
".to_string() + } + }, + _ => format!("
Unknown custom tag: {}
", tag_name, tag_name) + } + }).to_string(); + + processed +} + static AMMONIA: Lazy> = Lazy::new(|| { let mut builder = ammonia::Builder::default(); // All possible HTML Tags so that you can stylize via HTML @@ -156,7 +216,7 @@ static AMMONIA: Lazy> = Lazy::new(|| { builder.add_tag_attributes("pre", &["style"]); builder.add_tag_attributes("kbd", &["style"]); builder.add_tag_attributes("samp", &["style"]); - builder.add_tag_attributes("div", &["style"]); + builder.add_tag_attributes("div", &["style", "class"]); builder.add_tag_attributes("section", &["style"]); builder.add_tag_attributes("article", &["style"]); builder.add_tag_attributes("header", &["style"]); @@ -266,7 +326,7 @@ pub fn get_post_by_slug(slug: &str) -> Result> let created_at = get_file_creation_date(&file_path)?; - let processed_markdown = process_anchor_links(&result.content); + let processed_markdown = process_custom_tags(&process_anchor_links(&result.content)); let parser = Parser::new_ext(&processed_markdown, Options::all()); let mut html_output = String::new(); let mut heading_text = String::new(); @@ -439,4 +499,66 @@ pub fn save_post_cache_to_disk() { let _ = fs::create_dir_all("./cache"); let _ = fs::write(POST_STATS_PATH, map); } +} + +pub fn checkhealth() -> HealthReport { + let mut errors = Vec::new(); + let posts_dir = get_posts_directory(); + let posts_dir_exists = posts_dir.exists() && posts_dir.is_dir(); + let mut posts_count = 0; + if posts_dir_exists { + match std::fs::read_dir(&posts_dir) { + Ok(entries) => { + posts_count = entries.filter_map(|e| e.ok()) + .filter(|e| e.path().extension().map(|ext| ext == "md").unwrap_or(false)) + .count(); + }, + Err(e) => errors.push(format!("Failed to read posts dir: {}", e)), + } + } else { + errors.push("Posts directory does not exist".to_string()); + } + let cache_file_exists = Path::new(POSTS_CACHE_PATH).exists(); + let cache_stats_file_exists = Path::new(POST_STATS_PATH).exists(); + let (mut cache_readable, mut cache_post_count) = (false, None); + if cache_file_exists { + match std::fs::read_to_string(POSTS_CACHE_PATH) { + Ok(data) => { + match serde_json::from_str::>(&data) { + Ok(map) => { + cache_readable = true; + cache_post_count = Some(map.len()); + }, + Err(e) => errors.push(format!("Cache file not valid JSON: {}", e)), + } + }, + Err(e) => errors.push(format!("Failed to read cache file: {}", e)), + } + } + let (mut cache_stats_readable, mut cache_stats_count) = (false, None); + if cache_stats_file_exists { + match std::fs::read_to_string(POST_STATS_PATH) { + Ok(data) => { + match serde_json::from_str::>(&data) { + Ok(map) => { + cache_stats_readable = true; + cache_stats_count = Some(map.len()); + }, + Err(e) => errors.push(format!("Cache stats file not valid JSON: {}", e)), + } + }, + Err(e) => errors.push(format!("Failed to read cache stats file: {}", e)), + } + } + HealthReport { + posts_dir_exists, + posts_count, + cache_file_exists, + cache_stats_file_exists, + cache_readable, + cache_stats_readable, + cache_post_count, + cache_stats_count, + errors, + } } \ No newline at end of file diff --git a/posts/welcome.md b/posts/welcome.md index df7cd70..f41009c 100644 --- a/posts/welcome.md +++ b/posts/welcome.md @@ -24,6 +24,7 @@ author: Rattatwinko - [Features 🎉](#features) - [Administration 🚧](#administration) - [Customization 🎨](#customization) +- [Creating Posts with MdB ✍](#creating-posts-with-mdb) - [Troubleshooting 🚨](#troubleshooting) - [Support 🤝](#support) - [Support the Project ❤️](#support-the-project) @@ -31,7 +32,7 @@ author: Rattatwinko - [Folder Emojis 🇦🇹](#folder-emoji-technical-note) - [API 🏗️](#api) - [ToT, and Todo](#train-of-thought-for-this-project-and-todo) -- [Recent Changes](#) +- [Recent Changes](#recent-changes) --- @@ -302,6 +303,35 @@ The codebase is well-structured and documented. Key files: --- +## Creating Posts with MdB + +If you are reading posts. Then you probably dont need this explenation! + +Else you should read this. + +First of all, if you are creating posts within the terminal. then you should create posts with the following headers. + +```Markdown +--- +title: Welcome to MarkdownBlog +date: '2025-06-19' +tags: + - welcome + - introduction + - getting-started + - documentation +summary: A comprehensive guide to getting started with MarkdownBlog +author: Rattatwinko +--- +``` + +As you can see this is the header for the current Post. +You can write this like YML (idk). + +If you are writing posts within the Admin-Panel then you are a _lucky piece of shit_ cause there it does that **automatically** + +--- + ## Troubleshooting ### Common Issues @@ -450,4 +480,4 @@ If you are wondering: > > *"DEVELOPERS! DEVELOPERS! DEVELOPERS!"* - Steve Ballmer > -> — Rattatwinko, 2025 Q3 \ No newline at end of file +> — Rattatwinko, 2025 Q3 diff --git a/run-local-backend.sh b/run-local-backend.sh new file mode 100755 index 0000000..f281401 --- /dev/null +++ b/run-local-backend.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +# This script builds and runs the Rust backend locally, similar to the Docker container. +# Usage: ./run-local-backend.sh [args for markdown_backend] +# AnalSex with the frontend ( Cursor Autocompletion xD ) + +set -e + +# Set environment variables as in Docker (customize as needed) +export BLOG_OWNER=${BLOG_OWNER:-"rattatwinko"} + +# Build the backend in release mode +cd "$(dirname "$0")/markdown_backend" +echo "Building Rust backend..." +cargo build --release + +# Run the backend with any arguments passed to the script +cd target/release +echo "Running: ./markdown_backend $@" +./markdown_backend "$@" + +npm run dev ## start the fuckass frontend \ No newline at end of file diff --git a/src/app/admin/manage/rust-status/page.tsx b/src/app/admin/manage/rust-status/page.tsx index 315092e..964b4a7 100644 --- a/src/app/admin/manage/rust-status/page.tsx +++ b/src/app/admin/manage/rust-status/page.tsx @@ -9,10 +9,25 @@ interface PostStats { last_compile_time_ms: number; } +interface HealthReport { + posts_dir_exists: boolean; + posts_count: number; + cache_file_exists: boolean; + cache_stats_file_exists: boolean; + cache_readable: boolean; + cache_stats_readable: boolean; + cache_post_count?: number; + cache_stats_count?: number; + errors: string[]; +} + export default function RustStatusPage() { const [stats, setStats] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [health, setHealth] = useState(null); + const [healthLoading, setHealthLoading] = useState(true); + const [healthError, setHealthError] = useState(null); // Summary calculations const totalHits = stats.reduce((sum, s) => sum + s.cache_hits, 0); @@ -35,8 +50,24 @@ export default function RustStatusPage() { } }; + const fetchHealth = async () => { + setHealthLoading(true); + setHealthError(null); + try { + const res = await fetch('/api/admin/posts?checkhealth=1'); + if (!res.ok) throw new Error('Fehler beim Laden des Health-Checks'); + const data = await res.json(); + setHealth(data); + } catch (e: any) { + setHealthError(e.message || 'Unbekannter Fehler'); + } finally { + setHealthLoading(false); + } + }; + useEffect(() => { fetchStats(); + fetchHealth(); }, []); return ( @@ -88,7 +119,63 @@ export default function RustStatusPage() { - {/* Rest of your component remains the same */} + {/* Health Check Section */} +
+

Health-Check

+ {healthLoading &&
Lade Health-Check...
} + {healthError &&
{healthError}
} + {health && ( +
+
+
+ {health.posts_dir_exists ? '✔' : '✖'} + Posts-Verzeichnis +
+
+ {health.posts_count} + Posts +
+
+ {health.cache_file_exists ? '✔' : '✖'} + Cache-Datei +
+
+ {health.cache_stats_file_exists ? '✔' : '✖'} + Cache-Stats +
+
+ {health.cache_readable ? '✔' : '✖'} + Cache lesbar +
+
+ {health.cache_stats_readable ? '✔' : '✖'} + Stats lesbar +
+ {typeof health.cache_post_count === 'number' && ( +
+ {health.cache_post_count} + Cache-Posts +
+ )} + {typeof health.cache_stats_count === 'number' && ( +
+ {health.cache_stats_count} + Stats-Einträge +
+ )} +
+ {health.errors.length > 0 && ( +
+ Fehler: +
    + {health.errors.map((err, i) =>
  • {err}
  • )} +
+
+ )} +
+ )} +
+ {/* Summary Cards */}
diff --git a/src/app/api/admin/posts/route.ts b/src/app/api/admin/posts/route.ts index b2397c7..4b2fd27 100644 --- a/src/app/api/admin/posts/route.ts +++ b/src/app/api/admin/posts/route.ts @@ -70,6 +70,26 @@ export async function GET(request: Request) { }); } } + const checkhealth = searchParams.get('checkhealth'); + if (checkhealth === '1') { + // Call the Rust backend for health check + const rustResult = spawnSync( + process.cwd() + '/markdown_backend/target/release/markdown_backend', + ['checkhealth'], + { encoding: 'utf-8' } + ); + if (rustResult.status === 0 && rustResult.stdout) { + return new Response(rustResult.stdout, { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + } else { + return new Response(JSON.stringify({ error: rustResult.stderr || rustResult.error }), { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }); + } + } // Return the current pinned.json object try { const pinnedPath = path.join(process.cwd(), 'posts', 'pinned.json'); diff --git a/src/app/globals.css b/src/app/globals.css index 334b354..850807f 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -443,4 +443,16 @@ select:focus { .prose a { word-break: break-word; } -} \ No newline at end of file +} + +.custom-tag { + padding: 1rem; + border-radius: 6px; + margin: 1rem 0; + font-weight: 500; +} +.custom-tag.warning { background: #fff3cd; border: 1px solid #ffeaa7; color: #856404; } +.custom-tag.info { background: #d1ecf1; border: 1px solid #bee5eb; color: #0c5460; } +.custom-tag.success { background: #d4edda; border: 1px solid #c3e6cb; color: #155724; } +.custom-tag.error { background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; } +.custom-tag.mytag { background: #e3e3ff; border: 1px solid #b3b3ff; color: #333366; } \ No newline at end of file