diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..48c4314 --- /dev/null +++ b/build.rs @@ -0,0 +1,308 @@ +//! 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 Cargo.toml + +use std::fs; +use std::path::Path; + +fn main() { + let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); + let version = env!("CARGO_PKG_VERSION"); + 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}"); +} + +/// 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; + } + } + if dots >= 1 { + // C'est une version — remplacer + result.push_str(&format!("v{version}")); + i = j; + continue; + } + } + result.push(chars[i]); + i += 1; + } + + result +}