shields 1.3.0

High-performance Rust badge rendering engine, compatible with shields.io
Documentation
//! Build script for shields crate.
//!
//! 1. Minifies the SVG templates next to their sources (skipped when already up to date, so
//!    building from a read-only packaged source tree keeps working).
//! 2. Converts the JSON font width tables into static Rust arrays in OUT_DIR, so the library
//!    performs no JSON parsing at runtime.
use std::env;
use std::fmt::Write as _;
use std::fs;
use std::io;
use std::path::Path;

const TEMPLATE_FILES: [&str; 5] = [
    "templates/flat_badge_template.svg",
    "templates/flat_square_badge_template.svg",
    "templates/plastic_badge_template.svg",
    "templates/social_badge_template.svg",
    "templates/for_the_badge_template.svg",
];

const FONT_TABLES: [(&str, &str); 4] = [
    ("VERDANA_11_NORMAL", "assets/fonts/verdana-11px-normal.json"),
    ("HELVETICA_11_BOLD", "assets/fonts/helvetica-11px-bold.json"),
    ("VERDANA_10_NORMAL", "assets/fonts/verdana-10px-normal.json"),
    ("VERDANA_10_BOLD", "assets/fonts/verdana-10px-bold.json"),
];

fn main() -> io::Result<()> {
    println!("cargo:rerun-if-changed=build.rs");
    minify_templates()?;
    generate_font_tables()?;
    Ok(())
}

fn minify_templates() -> io::Result<()> {
    for file in &TEMPLATE_FILES {
        println!("cargo:rerun-if-changed={file}");

        let path = Path::new(file);
        let dest = path.with_extension("min.svg");

        let content = fs::read_to_string(path)?;
        let min_content = minify_svg(&content);
        // Skip the write when up to date: the packaged crate ships correct .min.svg files,
        // and its source tree may be read-only (docs.rs, Nix, shared registry caches).
        if fs::read_to_string(&dest).is_ok_and(|existing| existing == min_content) {
            continue;
        }
        fs::write(dest, min_content)?;
    }
    Ok(())
}

// Minify SVG content by trimming lines, joining whitespace, and removing unnecessary spaces
fn minify_svg(content: &str) -> String {
    let min_content = content.lines().map(str::trim).collect::<String>();
    let min_content = min_content.split_whitespace().collect::<Vec<_>>().join(" ");
    min_content.replace(" />", "/>").replace("> <", "><")
}

fn generate_font_tables() -> io::Result<()> {
    let out_dir = env::var("OUT_DIR").expect("OUT_DIR is set by cargo");
    let mut code = String::from(
        "// Generated by build.rs from assets/fonts/*.json. Do not edit.\n\
         // Sorted, non-overlapping (lower, upper, width) code point ranges.\n",
    );
    for (name, path) in &FONT_TABLES {
        println!("cargo:rerun-if-changed={path}");
        let json = fs::read_to_string(path)?;
        let ranges: Vec<(u32, u32, f64)> = serde_json::from_str(&json)
            .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
        assert!(
            ranges.windows(2).all(|w| w[0].1 < w[1].0),
            "{path}: font ranges must be sorted and non-overlapping"
        );
        writeln!(
            code,
            "pub static {name}: [(u32, u32, f64); {}] = [",
            ranges.len()
        )
        .unwrap();
        for (lower, upper, width) in ranges {
            writeln!(code, "    ({lower}, {upper}, {width:?}),").unwrap();
        }
        code.push_str("];\n");
    }
    fs::write(Path::new(&out_dir).join("font_tables.rs"), code)
}