- build.rs génère version dynamique : X.Y.Z+build.N-abc1234 - Version injectée dans l'API (IMAGOD_BUILD_VERSION) et pages HTML - Suffixe -dirty si modifications non commitées - replace_all_versions() consomme le suffixe build metadata - Suppression auto-changelog — reste manuel pour contrôle qualité
375 lines
13 KiB
Rust
375 lines
13 KiB
Rust
//! 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("<pre><code>");
|
|
html.push_str(&html_escape(&code_content));
|
|
html.push_str("</code></pre>\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!("<h3>{}</h3>\n", html_escape(&trimmed[4..])));
|
|
} else if trimmed.starts_with("## ") {
|
|
html.push_str(&format!("<h2>{}</h2>\n", html_escape(&trimmed[3..])));
|
|
} else if trimmed.starts_with("# ") {
|
|
html.push_str(&format!("<h1>{}</h1>\n", html_escape(&trimmed[2..])));
|
|
} else if trimmed == "---" {
|
|
html.push_str("<hr>\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!(
|
|
"<li><input type=\"checkbox\" checked disabled> {}</li>\n",
|
|
process_inline(&trimmed[5..].trim())
|
|
));
|
|
} else if trimmed.starts_with("- [ ]") {
|
|
html.push_str(&format!(
|
|
"<li><input type=\"checkbox\" disabled> {}</li>\n",
|
|
process_inline(&trimmed[5..].trim())
|
|
));
|
|
} else if trimmed.starts_with("- ") || trimmed.starts_with("* ") {
|
|
html.push_str(&format!("<li>{}</li>\n", process_inline(&trimmed[2..])));
|
|
} else {
|
|
html.push_str(&format!("<p>{}</p>\n", process_inline(trimmed)));
|
|
}
|
|
}
|
|
|
|
if in_code_block && !code_content.is_empty() {
|
|
html.push_str("<pre><code>");
|
|
html.push_str(&html_escape(&code_content));
|
|
html.push_str("</code></pre>\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("<tr>");
|
|
for cell in &content_cells {
|
|
html.push_str(&format!("<td>{}</td>", process_inline(cell.trim())));
|
|
}
|
|
html.push_str("</tr>\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<char> = 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}</{tag}>"));
|
|
} 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<char> = 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!("<a href=\"{url}\">{link_text}</a>"));
|
|
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<char> = 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
|
|
}
|