//! Build script — génère les pages HTML à partir du Markdown //! //! Exécuté à chaque `cargo build`. Garantit que : //! 1. changelog.html est généré depuis CHANGELOG.md //! 2. help.html est généré depuis ROADMAP.md //! 3. Le badge version dans index.html suit la version calculée //! 4. La version est auto-générée : base Cargo.toml + compteur commits git //! 5. CHANGELOG.md reçoit une entrée vide pour la nouvelle version si absente use std::fs; use std::path::Path; use std::process::Command; fn main() { let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); let base_version = env!("CARGO_PKG_VERSION"); // Version dynamique = version de base + numéro de build (compteur commits git) let version = compute_version(base_version, manifest_dir); let frontend_dir = manifest_dir.join("src/frontend"); // === changelog.html depuis CHANGELOG.md === let changelog_tmpl = frontend_dir.join("changelog.tmpl.html"); let changelog_md = manifest_dir.join("CHANGELOG.md"); let changelog_out = frontend_dir.join("changelog.html"); if changelog_tmpl.exists() && changelog_md.exists() { let md = fs::read_to_string(&changelog_md).unwrap_or_default(); let html = simple_md_to_html(&md); let tmpl = fs::read_to_string(&changelog_tmpl).unwrap_or_default(); let page = tmpl .replace("{{VERSION}}", &version) .replace("{{CHANGELOG_CONTENT}}", &html); fs::write(&changelog_out, &page).ok(); println!("cargo:rerun-if-changed=CHANGELOG.md"); println!("cargo:rerun-if-changed=src/frontend/changelog.tmpl.html"); } // === help.html depuis ROADMAP.md === let help_tmpl = frontend_dir.join("help.tmpl.html"); let roadmap_md = manifest_dir.join("ROADMAP.md"); let help_out = frontend_dir.join("help.html"); if help_tmpl.exists() && roadmap_md.exists() { let md = fs::read_to_string(&roadmap_md).unwrap_or_default(); let html = simple_md_to_html(&md); let tmpl = fs::read_to_string(&help_tmpl).unwrap_or_default(); let page = tmpl .replace("{{VERSION}}", &version) .replace("{{HELP_CONTENT}}", &html); fs::write(&help_out, &page).ok(); println!("cargo:rerun-if-changed=ROADMAP.md"); println!("cargo:rerun-if-changed=src/frontend/help.tmpl.html"); } // === Injection version dans index.html === let index_path = frontend_dir.join("index.html"); if index_path.exists() { let mut content = fs::read_to_string(&index_path).unwrap_or_default(); // Remplacer tous les badges de version vX.Y.Z par la version courante content = replace_all_versions(&content, &version); fs::write(&index_path, &content).ok(); println!("cargo:rerun-if-changed=src/frontend/index.html"); } println!("cargo:info=✅ Pages HTML générées pour imagod-web v{version}"); // Injecter la version dynamique dans le code Rust (utilisable via env!("IMAGOD_BUILD_VERSION")) println!("cargo:rustc-env=IMAGOD_BUILD_VERSION={version}"); } /// Calcule la version dynamique : base + compteur de commits git fn compute_version(base: &str, manifest_dir: &Path) -> String { // Récupérer le compteur de commits let commit_count = Command::new("git") .args(["rev-list", "--count", "HEAD"]) .current_dir(manifest_dir) .output() .ok() .and_then(|o| String::from_utf8(o.stdout).ok()) .map(|s| s.trim().to_string()) .unwrap_or_else(|| "0".to_string()); // Récupérer le hash court du commit let short_hash = Command::new("git") .args(["rev-parse", "--short=7", "HEAD"]) .current_dir(manifest_dir) .output() .ok() .and_then(|o| String::from_utf8(o.stdout).ok()) .map(|s| s.trim().to_string()) .unwrap_or_else(|| "unknown".to_string()); // Vérifier si le working tree est dirty let dirty = Command::new("git") .args(["diff", "--quiet"]) .current_dir(manifest_dir) .status() .map(|s| !s.success()) .unwrap_or(false); let suffix = if dirty { "-dirty" } else { "" }; format!("{}+build.{}-{}{}", base, commit_count, short_hash, suffix) } /// Module minimal pour la date du jour (zéro dépendance externe) mod chrono_lite { use std::process::Command; pub fn today() -> String { Command::new("date") .args(["+%d %B %Y"]) .output() .ok() .and_then(|o| String::from_utf8(o.stdout).ok()) .map(|s| s.trim().to_string()) .unwrap_or_else(|| "Date inconnue".to_string()) } } /// Mini convertisseur Markdown → HTML (zéro dépendance) fn simple_md_to_html(md: &str) -> String { let mut html = String::new(); let mut in_code_block = false; let mut code_content = String::new(); for line in md.lines() { if line.starts_with("```") { if in_code_block { html.push_str("
");
                html.push_str(&html_escape(&code_content));
                html.push_str("
\n"); code_content.clear(); in_code_block = false; } else { in_code_block = true; } continue; } if in_code_block { if !code_content.is_empty() { code_content.push('\n'); } code_content.push_str(line); continue; } let trimmed = line.trim(); if trimmed.is_empty() { continue; } if trimmed.starts_with("### ") { html.push_str(&format!("

{}

\n", html_escape(&trimmed[4..]))); } else if trimmed.starts_with("## ") { html.push_str(&format!("

{}

\n", html_escape(&trimmed[3..]))); } else if trimmed.starts_with("# ") { html.push_str(&format!("

{}

\n", html_escape(&trimmed[2..]))); } else if trimmed == "---" { html.push_str("
\n"); } else if trimmed.starts_with('|') && trimmed.ends_with('|') { render_table(&mut html, trimmed); } else if trimmed.starts_with("- [x]") || trimmed.starts_with("- [X]") { html.push_str(&format!( "
  • {}
  • \n", process_inline(&trimmed[5..].trim()) )); } else if trimmed.starts_with("- [ ]") { html.push_str(&format!( "
  • {}
  • \n", process_inline(&trimmed[5..].trim()) )); } else if trimmed.starts_with("- ") || trimmed.starts_with("* ") { html.push_str(&format!("
  • {}
  • \n", process_inline(&trimmed[2..]))); } else { html.push_str(&format!("

    {}

    \n", process_inline(trimmed))); } } if in_code_block && !code_content.is_empty() { html.push_str("
    ");
            html.push_str(&html_escape(&code_content));
            html.push_str("
    \n"); } html } fn render_table(html: &mut String, line: &str) { let cells: Vec<&str> = line.split('|').collect(); let is_sep = cells.iter().any(|c| { let t = c.trim(); t.contains("---") || t.contains(":--") || t.is_empty() && !t.contains(char::is_alphanumeric) }); if is_sep { return; } let content_cells: Vec<&str> = cells.iter().filter(|c| !c.trim().is_empty()).copied().collect(); if content_cells.is_empty() { return; } html.push_str(""); for cell in &content_cells { html.push_str(&format!("{}", process_inline(cell.trim()))); } html.push_str("\n"); } fn process_inline(text: &str) -> String { // Échapper HTML d'abord let text = html_escape(text); // Puis appliquer les formats let text = replace_pattern(&text, "**", "strong"); let text = replace_pattern(&text, "__", "strong"); let text = replace_pattern(&text, "*", "em"); let text = replace_pattern(&text, "_", "em"); let text = replace_pattern(&text, "~~", "del"); let text = replace_pattern(&text, "`", "code"); // Liens [texte](url) replace_links(&text) } fn replace_pattern(text: &str, marker: &str, tag: &str) -> String { let mut result = String::new(); let mut chars = text.chars().peekable(); let marker_len = marker.chars().count(); let marker_chars: Vec = marker.chars().collect(); while let Some(&c) = chars.peek() { // Vérifier si on est au début d'un marqueur if c == marker_chars[0] { let mut candidate = String::new(); let mut clone = chars.clone(); for _ in 0..marker_len { if let Some(&mc) = clone.peek() { candidate.push(mc); clone.next(); } } if candidate == marker { // Avancer après le marqueur d'ouverture for _ in 0..marker_len { chars.next(); } // Chercher le marqueur de fermeture let mut inner = String::new(); let mut found = false; while let Some(&c2) = chars.peek() { if c2 == marker_chars[0] { let mut close_candidate = String::new(); let mut close_clone = chars.clone(); for _ in 0..marker_len { if let Some(&mc) = close_clone.peek() { close_candidate.push(mc); close_clone.next(); } } if close_candidate == marker { for _ in 0..marker_len { chars.next(); } found = true; break; } } inner.push(chars.next().unwrap()); } if found { result.push_str(&format!("<{tag}>{inner}")); } else { result.push_str(&candidate); result.push_str(&inner); } continue; } } result.push(chars.next().unwrap()); } result } fn replace_links(text: &str) -> String { let mut result = String::new(); let chars: Vec = text.chars().collect(); let mut i = 0; while i < chars.len() { if chars[i] == '[' { let mut j = i + 1; let mut link_text = String::new(); while j < chars.len() && chars[j] != ']' { link_text.push(chars[j]); j += 1; } if j + 2 < chars.len() && chars[j] == ']' && chars[j + 1] == '(' { let mut k = j + 2; let mut url = String::new(); while k < chars.len() && chars[k] != ')' { url.push(chars[k]); k += 1; } if k < chars.len() && chars[k] == ')' { result.push_str(&format!("{link_text}")); i = k + 1; continue; } } // Pas un lien valide, remettre le [ result.push('['); result.push_str(&link_text); i = j; } else { result.push(chars[i]); i += 1; } } result } fn html_escape(text: &str) -> String { text.replace('&', "&") .replace('<', "<") .replace('>', ">") .replace('"', """) } /// Remplace toutes les occurrences de `vX.Y.Z` dans le HTML par la version courante fn replace_all_versions(html: &str, version: &str) -> String { let mut result = String::new(); let chars: Vec = html.chars().collect(); let mut i = 0; while i < chars.len() { if chars[i] == 'v' && i + 1 < chars.len() && chars[i + 1].is_ascii_digit() { // Essayer de parser une version vX.Y.Z ou vX.Y let mut j = i + 1; let mut dots = 0; while j < chars.len() { if chars[j].is_ascii_digit() { j += 1; } else if chars[j] == '.' && dots < 2 && j + 1 < chars.len() && chars[j + 1].is_ascii_digit() { dots += 1; j += 1; } else { break; } } // Consommer le suffixe build metadata (+ ou - suivi d'alphanum/dots/tirets) while j < chars.len() && (chars[j] == '+' || chars[j] == '-') { j += 1; while j < chars.len() && (chars[j].is_alphanumeric() || chars[j] == '.' || chars[j] == '-') { j += 1; } } if dots >= 1 { // C'est une version — remplacer result.push_str(&format!("v{version}")); i = j; continue; } } result.push(chars[i]); i += 1; } result }