use std::collections::HashMap;
use std::fs;
use std::path::Path;
use walkdir::WalkDir;
use pulldown_cmark::{Parser, html};
use serde::Deserialize;
use tera::{Tera, Context};
use serde_json::json;
#[derive(Debug, Deserialize)]
struct Config {
theme: Option<String>,
}
#[derive(Debug, Deserialize, Default)]
struct Frontmatter {
title: Option<String>,
date: Option<String>,
template: Option<String>,
draft: Option<bool>,
}
fn main() {
let input_root = Path::new("content");
let output_root = Path::new("public");
let config = load_config("veltox.toml");
let theme_name = config.theme.unwrap_or_else(|| "docs".to_string());
let template_path = format!("themes/{}/templates/**/*", theme_name);
let tera = Tera::new(&template_path).expect("Failed to load templates");
let css_src = format!("themes/{}/theme.css", theme_name);
let css_dst = output_root.join("theme.css");
if Path::new(&css_src).exists() {
fs::create_dir_all(output_root).expect("Failed to create public directory");
fs::copy(&css_src, &css_dst).expect("Failed to copy theme.css");
}
let template_dir = format!("themes/{}/templates", theme_name);
if Path::new(&template_dir).exists() {
for entry in WalkDir::new(&template_dir)
.into_iter()
.filter_map(Result::ok)
.filter(|e| e.path().extension().map_or(false, |ext| ext == "html"))
{
let src = entry.path();
let filename = src.file_name().unwrap();
let dst = output_root.join(filename);
fs::copy(src, dst).expect("Failed to copy template file");
}
}
let sidebar = generate_sidebar(input_root);
let mut search_index = Vec::new();
for entry in WalkDir::new(input_root)
.into_iter()
.filter_map(Result::ok)
.filter(|e| e.path().extension().map_or(false, |ext| ext == "md"))
{
let input_path = entry.path();
let relative_path = input_path.strip_prefix(input_root).unwrap();
let mut output_path = output_root.join(relative_path);
output_path.set_extension("html");
if let Some(parent) = output_path.parent() {
fs::create_dir_all(parent).expect("Failed to create output directory");
}
let raw = fs::read_to_string(input_path).expect("Failed to read markdown file");
let (frontmatter, markdown) = split_frontmatter(&raw);
if frontmatter.draft.unwrap_or(false) {
println!("Skipping draft: {:?}", input_path);
continue;
}
let parser = Parser::new(markdown);
let mut html_output = String::new();
html::push_html(&mut html_output, parser);
let mut context = Context::new();
context.insert("title", &frontmatter.title);
context.insert("date", &frontmatter.date);
context.insert("content", &html_output);
context.insert("sidebar", &sidebar);
let template_name = frontmatter.template.unwrap_or_else(|| "page.html".to_string());
let rendered = tera.render(&template_name, &context).expect("Template rendering failed");
fs::write(output_path, rendered).expect("Failed to write HTML file");
let url = relative_path.with_extension("html").to_string_lossy().replace('\\', "/");
let title = frontmatter.title.unwrap_or_else(|| {
relative_path.file_stem().unwrap().to_string_lossy().to_string()
});
search_index.push(json!({
"title": title,
"url": format!("./{}", url),
"content": markdown,
}));
}
let index_json = serde_json::to_string_pretty(&search_index).expect("Failed to serialize search index");
fs::write(output_root.join("search-index.json"), index_json).expect("Failed to write search index");
println!("✅ Veltox build complete with theme: {}", theme_name);
}
fn split_frontmatter(content: &str) -> (Frontmatter, &str) {
if content.starts_with("---") {
let parts: Vec<&str> = content.splitn(3, "---").collect();
if parts.len() == 3 {
let fm_str = parts[1];
let md_str = parts[2];
let frontmatter: Frontmatter = serde_yaml::from_str(fm_str).unwrap_or_default();
return (frontmatter, md_str.trim());
}
}
(Frontmatter::default(), content)
}
fn load_config(path: &str) -> Config {
if let Ok(raw) = fs::read_to_string(path) {
toml::from_str(&raw).unwrap_or(Config { theme: None })
} else {
Config { theme: None }
}
}
fn generate_sidebar(input_root: &Path) -> Vec<HashMap<String, String>> {
let mut sidebar = Vec::new();
for entry in WalkDir::new(input_root)
.into_iter()
.filter_map(Result::ok)
.filter(|e| e.path().extension().map_or(false, |ext| ext == "md"))
{
let path = entry.path();
let relative = path.strip_prefix(input_root).unwrap();
let url = relative.with_extension("html").to_string_lossy().replace('\\', "/");
let raw = fs::read_to_string(path).unwrap_or_default();
let (frontmatter, _) = split_frontmatter(&raw);
let title = frontmatter.title.unwrap_or_else(|| {
relative.file_stem().unwrap().to_string_lossy().to_string()
});
let mut item = HashMap::new();
item.insert("title".to_string(), title);
item.insert("url".to_string(), format!("./{}", url));
sidebar.push(item);
}
sidebar
}