use axum::extract::State;
use axum::response::Html;
use std::sync::Arc;
use crate::{AppState, escape};
pub async fn trigger(State(state): State<Arc<AppState>>) -> Html<String> {
match crate::rebuild_site(&state) {
Ok(()) => Html(r#"<span style="color: #34d399;">Built successfully.</span>"#.to_string()),
Err(e) => Html(format!(
r#"<span style="color: #f87171;">Build error: {}</span>"#,
escape(&e)
)),
}
}
pub async fn render_preview(State(state): State<Arc<AppState>>, body: String) -> Html<String> {
let content = strip_frontmatter(&body);
let (md_config, base_url) = load_md_config_and_base(&state);
let shortcode_dir = state.root.join("templates/shortcodes");
let sandbox_root = state.sandbox.as_deref().unwrap_or(&state.root);
let (after_shortcodes, shortcode_error) = match zorto_core::shortcodes::process_shortcodes(
&content,
&shortcode_dir,
&state.root,
sandbox_root,
) {
Ok(rendered) => (rendered, None),
Err(e) => (content.clone(), Some(e.to_string())),
};
let mut blocks = Vec::new();
let html = zorto_core::markdown::render_markdown(
&after_shortcodes,
&md_config,
&mut blocks,
&base_url,
);
let exec_count = blocks.len();
drop(blocks);
let html = stub_exec_placeholders(html, exec_count);
let disclaimer = build_disclaimer(exec_count, shortcode_error.as_deref());
if disclaimer.is_empty() {
Html(html)
} else {
Html(format!("{disclaimer}{html}"))
}
}
fn load_md_config_and_base(state: &AppState) -> (zorto_core::config::MarkdownConfig, String) {
let config_path = state.root.join("config.toml");
let raw = std::fs::read_to_string(&config_path).unwrap_or_default();
match toml::from_str::<zorto_core::config::Config>(&raw) {
Ok(cfg) => (cfg.markdown, cfg.base_url),
Err(_) => (zorto_core::config::MarkdownConfig::default(), String::new()),
}
}
fn stub_exec_placeholders(html: String, count: usize) -> String {
let mut result = html;
for i in 0..count {
let placeholder = format!("<!-- EXEC_BLOCK_{i} -->");
if !result.contains(&placeholder) {
continue;
}
let replacement = r#"<div class="code-block-preview-suppressed"><div class="preview-suppressed-pill">code execution suppressed in preview — Save & Rebuild to run this block</div></div>"#;
result = result.replacen(&placeholder, replacement, 1);
}
result
}
fn build_disclaimer(exec_count: usize, shortcode_error: Option<&str>) -> String {
let mut parts: Vec<String> = Vec::new();
if let Some(err) = shortcode_error {
parts.push(format!(
r#"shortcode error — preview fell back to raw text: <code>{}</code>"#,
escape(err)
));
}
if exec_count > 0 {
parts.push(format!(
"{exec_count} executable code block{plural} not run",
plural = if exec_count == 1 { "" } else { "s" }
));
}
if parts.is_empty() {
return String::new();
}
format!(
r#"<div class="preview-disclaimer">Preview is fragment-only: {}. Save & Rebuild to see the full output.</div>"#,
parts.join("; ")
)
}
fn strip_frontmatter(content: &str) -> String {
let trimmed = content.trim();
if let Some(rest) = trimmed.strip_prefix("+++") {
if let Some(end) = rest.find("\n+++") {
return rest[end + 4..].to_string();
}
}
content.to_string()
}