You've already forked cpp-httplib
- Added a favicon link to all tour pages in the Japanese documentation. - Updated navigation links to include SVG icons for Home and GitHub. - Changed language button to include an SVG icon for better visual representation. - Improved theme toggle button to use SVG icons for light and dark modes. - Refactored the documentation build commands in the justfile for clarity and consistency.
395 lines
12 KiB
Rust
395 lines
12 KiB
Rust
use crate::config::{NavLink, SiteConfig};
|
|
use crate::defaults;
|
|
use crate::markdown::{Frontmatter, MarkdownRenderer};
|
|
use anyhow::{Context, Result};
|
|
use serde::Serialize;
|
|
use std::fs;
|
|
use std::path::{Path, PathBuf};
|
|
use tera::Tera;
|
|
use walkdir::WalkDir;
|
|
|
|
#[derive(Debug, Serialize)]
|
|
struct PageContext {
|
|
title: String,
|
|
url: String,
|
|
status: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Clone)]
|
|
struct NavItem {
|
|
title: String,
|
|
url: String,
|
|
children: Vec<NavItem>,
|
|
active: bool,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
struct SiteContext {
|
|
title: String,
|
|
version: Option<String>,
|
|
base_url: String,
|
|
base_path: String,
|
|
langs: Vec<String>,
|
|
nav: Vec<NavLink>,
|
|
}
|
|
|
|
struct Page {
|
|
frontmatter: Frontmatter,
|
|
html_content: String,
|
|
url: String,
|
|
out_path: PathBuf,
|
|
rel_path: String,
|
|
section: String,
|
|
}
|
|
|
|
pub fn build(src: &Path, out: &Path) -> Result<()> {
|
|
let config = SiteConfig::load(src)?;
|
|
let renderer = MarkdownRenderer::new(config.highlight_theme(), config.highlight_theme_light());
|
|
|
|
// Build Tera: start with embedded defaults, then override with user templates
|
|
let tera = build_tera(src)?;
|
|
|
|
// Clean output directory
|
|
if out.exists() {
|
|
fs::remove_dir_all(out).context("Failed to clean output directory")?;
|
|
}
|
|
fs::create_dir_all(out)?;
|
|
|
|
// Copy static files: embedded defaults first, then user overrides on top
|
|
copy_default_static(out)?;
|
|
let static_dir = src.join("static");
|
|
if static_dir.exists() {
|
|
copy_dir_recursive(&static_dir, out)?;
|
|
}
|
|
|
|
// Build each language
|
|
for lang in &config.i18n.langs {
|
|
let pages_dir = src.join("pages").join(lang);
|
|
if !pages_dir.exists() {
|
|
eprintln!("Warning: pages directory not found for lang '{}', skipping", lang);
|
|
continue;
|
|
}
|
|
|
|
let pages = collect_pages(&pages_dir, lang, out, &renderer, &config.site.base_path)?;
|
|
let nav = build_nav(&pages);
|
|
|
|
for page in &pages {
|
|
let template_name = if page.section.is_empty() {
|
|
"portal.html"
|
|
} else {
|
|
"page.html"
|
|
};
|
|
|
|
// Filter nav to only the current section
|
|
let section_nav: Vec<&NavItem> = nav
|
|
.iter()
|
|
.filter(|item| {
|
|
let item_section = extract_section(&item.url, &config.site.base_path);
|
|
item_section == page.section
|
|
})
|
|
.collect();
|
|
|
|
let mut ctx = tera::Context::new();
|
|
ctx.insert("page", &PageContext {
|
|
title: page.frontmatter.title.clone(),
|
|
url: page.url.clone(),
|
|
status: page.frontmatter.status.clone(),
|
|
});
|
|
ctx.insert("content", &page.html_content);
|
|
ctx.insert("lang", lang);
|
|
ctx.insert("site", &SiteContext {
|
|
title: config.site.title.clone(),
|
|
version: config.site.version.clone(),
|
|
base_url: config.site.base_url.clone(),
|
|
base_path: config.site.base_path.clone(),
|
|
langs: config.i18n.langs.clone(),
|
|
nav: config.nav.clone(),
|
|
});
|
|
|
|
// Set active state and pass nav
|
|
let mut nav_with_active: Vec<NavItem> = section_nav
|
|
.into_iter()
|
|
.cloned()
|
|
.map(|mut item| {
|
|
set_active(&mut item, &page.url);
|
|
item
|
|
})
|
|
.collect();
|
|
|
|
// If we're on a section index page, expand its children
|
|
if let Some(item) = nav_with_active.first_mut() {
|
|
if item.url == page.url {
|
|
item.active = true;
|
|
}
|
|
}
|
|
|
|
ctx.insert("nav", &nav_with_active);
|
|
|
|
let html = tera
|
|
.render(template_name, &ctx)
|
|
.with_context(|| format!("Failed to render template for {}", page.url))?;
|
|
|
|
if let Some(parent) = page.out_path.parent() {
|
|
fs::create_dir_all(parent)?;
|
|
}
|
|
fs::write(&page.out_path, html)?;
|
|
}
|
|
}
|
|
|
|
// Generate root redirect
|
|
generate_root_redirect(out, &config)?;
|
|
|
|
println!(
|
|
"Site generated: {} languages, output at {}",
|
|
config.i18n.langs.len(),
|
|
out.display()
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn collect_pages(
|
|
pages_dir: &Path,
|
|
lang: &str,
|
|
out: &Path,
|
|
renderer: &MarkdownRenderer,
|
|
base_path: &str,
|
|
) -> Result<Vec<Page>> {
|
|
let mut pages = Vec::new();
|
|
|
|
for entry in WalkDir::new(pages_dir)
|
|
.into_iter()
|
|
.filter_map(|e| e.ok())
|
|
.filter(|e| {
|
|
e.path().extension().map_or(false, |ext| ext == "md")
|
|
})
|
|
{
|
|
let path = entry.path();
|
|
let content = fs::read_to_string(path)
|
|
.with_context(|| format!("Failed to read {}", path.display()))?;
|
|
|
|
let (frontmatter, body) = MarkdownRenderer::parse_frontmatter(&content)
|
|
.with_context(|| format!("Failed to parse frontmatter in {}", path.display()))?;
|
|
|
|
let html_content = renderer.render(body);
|
|
|
|
let rel = path.strip_prefix(pages_dir)?;
|
|
let rel_str = rel.to_string_lossy().to_string();
|
|
|
|
// Compute URL and output path
|
|
let (url, out_path) = if rel.file_name().map_or(false, |f| f == "index.md") {
|
|
// index.md -> <base_path>/<lang>/dir/
|
|
let parent = rel.parent().unwrap_or(Path::new(""));
|
|
if parent.as_os_str().is_empty() {
|
|
// Root index.md
|
|
(
|
|
format!("{}/{}/", base_path, lang),
|
|
out.join(lang).join("index.html"),
|
|
)
|
|
} else {
|
|
(
|
|
format!("{}/{}/{}/", base_path, lang, parent.display()),
|
|
out.join(lang).join(parent).join("index.html"),
|
|
)
|
|
}
|
|
} else {
|
|
// foo.md -> <base_path>/<lang>/foo/
|
|
let stem = rel.with_extension("");
|
|
(
|
|
format!("{}/{}/{}/", base_path, lang, stem.display()),
|
|
out.join(lang).join(&stem).join("index.html"),
|
|
)
|
|
};
|
|
|
|
let section = extract_section(&url, base_path);
|
|
|
|
pages.push(Page {
|
|
frontmatter,
|
|
html_content,
|
|
url,
|
|
out_path,
|
|
rel_path: rel_str,
|
|
section,
|
|
});
|
|
}
|
|
|
|
Ok(pages)
|
|
}
|
|
|
|
fn extract_section(url: &str, base_path: &str) -> String {
|
|
// Strip base_path prefix before parsing
|
|
let stripped = url.strip_prefix(base_path).unwrap_or(url);
|
|
// URL format: /<lang>/ or /<lang>/section/...
|
|
let parts: Vec<&str> = stripped.trim_matches('/').split('/').collect();
|
|
if parts.len() >= 2 {
|
|
parts[1].to_string()
|
|
} else {
|
|
String::new()
|
|
}
|
|
}
|
|
|
|
fn build_nav(pages: &[Page]) -> Vec<NavItem> {
|
|
// Group pages by section (top-level directory)
|
|
let mut sections: std::collections::BTreeMap<String, Vec<&Page>> =
|
|
std::collections::BTreeMap::new();
|
|
|
|
for page in pages {
|
|
if page.section.is_empty() {
|
|
continue; // Skip root index (portal)
|
|
}
|
|
sections
|
|
.entry(page.section.clone())
|
|
.or_default()
|
|
.push(page);
|
|
}
|
|
|
|
let mut nav = Vec::new();
|
|
|
|
for (section, mut section_pages) in sections {
|
|
// Sort by order, then by filename
|
|
section_pages.sort_by(|a, b| {
|
|
a.frontmatter
|
|
.order
|
|
.cmp(&b.frontmatter.order)
|
|
.then_with(|| a.rel_path.cmp(&b.rel_path))
|
|
});
|
|
|
|
// Find the section index page
|
|
let index_page = section_pages
|
|
.iter()
|
|
.find(|p| p.rel_path.ends_with("index.md") && p.section == section);
|
|
|
|
let section_title = index_page
|
|
.map(|p| p.frontmatter.title.clone())
|
|
.unwrap_or_else(|| section.clone());
|
|
let section_url = index_page
|
|
.map(|p| p.url.clone())
|
|
.unwrap_or_default();
|
|
|
|
let children: Vec<NavItem> = section_pages
|
|
.iter()
|
|
.filter(|p| !p.rel_path.ends_with("index.md") || p.section != section)
|
|
.map(|p| NavItem {
|
|
title: p.frontmatter.title.clone(),
|
|
url: p.url.clone(),
|
|
children: Vec::new(),
|
|
active: false,
|
|
})
|
|
.collect();
|
|
|
|
nav.push(NavItem {
|
|
title: section_title,
|
|
url: section_url,
|
|
children,
|
|
active: false,
|
|
});
|
|
}
|
|
|
|
// Sort nav sections by order of their index pages
|
|
nav
|
|
}
|
|
|
|
fn set_active(item: &mut NavItem, current_url: &str) {
|
|
if item.url == current_url {
|
|
item.active = true;
|
|
}
|
|
for child in &mut item.children {
|
|
set_active(child, current_url);
|
|
if child.active {
|
|
item.active = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn generate_root_redirect(out: &Path, config: &SiteConfig) -> Result<()> {
|
|
let base_path = &config.site.base_path;
|
|
let html = format!(
|
|
r#"<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<script>
|
|
(function() {{
|
|
var lang = localStorage.getItem('preferred-lang') || '{default_lang}';
|
|
window.location.replace('{base_path}/' + lang + '/');
|
|
}})();
|
|
</script>
|
|
<meta http-equiv="refresh" content="0;url={base_path}/{default_lang}/">
|
|
<title>Redirecting...</title>
|
|
</head>
|
|
<body>
|
|
<p>Redirecting to <a href="{base_path}/{default_lang}/">{base_path}/{default_lang}/</a>...</p>
|
|
</body>
|
|
</html>"#,
|
|
default_lang = config.i18n.default_lang,
|
|
base_path = base_path,
|
|
);
|
|
|
|
fs::write(out.join("index.html"), html)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Build Tera with embedded default templates, then override with any files
|
|
/// found in `<src>/templates/`.
|
|
fn build_tera(src: &Path) -> Result<Tera> {
|
|
let mut tera = Tera::default();
|
|
|
|
// Register embedded defaults
|
|
for (name, source) in defaults::default_templates() {
|
|
tera.add_raw_template(name, source)
|
|
.with_context(|| format!("Failed to add default template '{}'", name))?;
|
|
}
|
|
|
|
// Override with user-provided templates (if any)
|
|
let templates_dir = src.join("templates");
|
|
if templates_dir.exists() {
|
|
for entry in WalkDir::new(&templates_dir)
|
|
.into_iter()
|
|
.filter_map(|e| e.ok())
|
|
.filter(|e| e.path().extension().map_or(false, |ext| ext == "html"))
|
|
{
|
|
let path = entry.path();
|
|
let rel = path.strip_prefix(&templates_dir)?;
|
|
let name = rel.to_string_lossy().replace('\\', "/");
|
|
let source = fs::read_to_string(path)
|
|
.with_context(|| format!("Failed to read template {}", path.display()))?;
|
|
tera.add_raw_template(&name, &source)
|
|
.with_context(|| format!("Failed to register template '{}'", name))?;
|
|
}
|
|
}
|
|
|
|
Ok(tera)
|
|
}
|
|
|
|
/// Write embedded default static files (css/js) to the output directory.
|
|
fn copy_default_static(out: &Path) -> Result<()> {
|
|
for (rel_path, content) in defaults::default_static_files() {
|
|
let target = out.join(rel_path);
|
|
if let Some(parent) = target.parent() {
|
|
fs::create_dir_all(parent)?;
|
|
}
|
|
// Only write if not already present (user file takes precedence via
|
|
// the subsequent copy_dir_recursive call, but write defaults first)
|
|
fs::write(&target, content)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
|
|
for entry in WalkDir::new(src).into_iter().filter_map(|e| e.ok()) {
|
|
let path = entry.path();
|
|
let rel = path.strip_prefix(src)?;
|
|
let target = dst.join(rel);
|
|
|
|
if path.is_dir() {
|
|
fs::create_dir_all(&target)?;
|
|
} else {
|
|
if let Some(parent) = target.parent() {
|
|
fs::create_dir_all(parent)?;
|
|
}
|
|
fs::copy(path, &target)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|