badgelib 0.4.0

A library for generating badges in Rust
Documentation
use std::path::Path;
use std::{fs, io};

use badgelib::{Animation, Badge, Color, Period};

#[derive(Clone, Copy, PartialEq, Eq)]
enum Section {
  Gradients,
  CustomIcons,
  Animations,
  BuiltIns,
}

impl Section {
  const ALL: [Self; 4] = [Self::Gradients, Self::CustomIcons, Self::Animations, Self::BuiltIns];

  fn name(self) -> &'static str {
    match self {
      Self::Gradients => "gradients",
      Self::CustomIcons => "custom-icons",
      Self::Animations => "animations",
      Self::BuiltIns => "built-ins",
    }
  }
}

struct Example {
  filename: &'static str,
  alt: &'static str,
  section: Section,
  badge: Badge,
}

fn hex(value: &str) -> Color {
  Color::try_from(value).expect("example colors must be valid")
}

fn update_readme(root: &Path, examples: &[Example]) -> io::Result<()> {
  let path = root.join("readme.md");
  let mut readme = fs::read_to_string(&path)?;

  for section in Section::ALL {
    let name = section.name();
    let start = format!("<!-- examples:{name}:start -->");
    let end = format!("<!-- examples:{name}:end -->");
    let starts = readme.match_indices(&start).map(|(index, _)| index).collect::<Vec<_>>();
    let ends = readme.match_indices(&end).map(|(index, _)| index).collect::<Vec<_>>();

    if starts.len() != 1 || ends.len() != 1 || starts[0] >= ends[0] {
      return Err(io::Error::new(
        io::ErrorKind::InvalidData,
        format!("readme.md must contain exactly one ordered {start} / {end} pair"),
      ));
    }

    let images = examples
      .iter()
      .filter(|example| example.section == section)
      .map(|example| {
        format!("  <img src=\"examples/{}\" alt=\"{}\">", example.filename, example.alt)
      })
      .collect::<Vec<_>>();
    if images.is_empty() {
      return Err(io::Error::new(
        io::ErrorKind::InvalidData,
        format!("no examples declared for README section {name}"),
      ));
    }

    let replacement = format!("{start}\n<p align=\"center\">\n{}\n</p>\n{end}", images.join("\n"));
    readme.replace_range(starts[0]..ends[0] + end.len(), &replacement);
  }

  fs::write(path, readme)
}

fn main() -> io::Result<()> {
  let root = Path::new(env!("CARGO_MANIFEST_DIR"));
  let output_dir = root.join("examples");
  fs::create_dir_all(&output_dir)?;

  for entry in fs::read_dir(&output_dir)? {
    let path = entry?.path();
    if path.extension().is_some_and(|extension| extension == "svg") {
      fs::remove_file(path)?;
    }
  }

  let examples = [
    Example {
      filename: "value.svg",
      alt: "Value gradient",
      section: Section::Gradients,
      badge: Badge::new()
        .label("version")
        .value("v1.2.3")
        .value_gradient([hex("fb7185"), hex("f97316"), hex("facc15")])
        .logo("rust"),
    },
    Example {
      filename: "dual.svg",
      alt: "Dual gradient",
      section: Section::Gradients,
      badge: Badge::new()
        .label("release")
        .label_gradient([hex("334155"), hex("7c3aed")])
        .value("stable")
        .value_gradient([hex("ec4899"), hex("f97316")])
        .logo("github"),
    },
    Example {
      filename: "mono.svg",
      alt: "Mono gradient",
      section: Section::Gradients,
      badge: Badge::new()
        .value("gradient badge")
        .value_gradient([hex("f43f5e"), hex("a855f7"), hex("06b6d4")])
        .logo("rust"),
    },
    Example {
      filename: "custom-icon.svg",
      alt: "Custom icon",
      section: Section::CustomIcons,
      badge: Badge::new().value("custom icon").icon_svg(
        r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
          <path d="M12 3 L14 10 L21 12 L14 14 L12 21 L10 14 L3 12 L10 10 Z" fill="#facc15" />
        </svg>"##,
      ),
    },
    Example {
      filename: "value-gradient-animated.svg",
      alt: "Flow (value side)",
      section: Section::Animations,
      badge: Badge::new()
        .label("build")
        .value("flowing")
        .value_gradient([hex("a855f7"), hex("3b82f6"), hex("ec4899"), hex("06b6d4")])
        .animation(Animation::Flow)
        .logo("rust"),
    },
    Example {
      filename: "label-gradient-animated.svg",
      alt: "Flow (label side)",
      section: Section::Animations,
      badge: Badge::new()
        .label("flowing")
        .value("build")
        .label_gradient([hex("a855f7"), hex("3b82f6"), hex("ec4899"), hex("06b6d4")])
        .value_color(hex("18181b"))
        .animation(Animation::Flow)
        .logo("rust"),
    },
    Example {
      filename: "dual-gradient-animated.svg",
      alt: "Flow (both sides)",
      section: Section::Animations,
      badge: Badge::new()
        .label("release")
        .label_gradient([hex("334155"), hex("7c3aed")])
        .value("stable")
        .value_gradient([hex("ec4899"), hex("f97316")])
        .animation(Animation::Flow)
        .logo("github"),
    },
    Example {
      filename: "shine-animated.svg",
      alt: "Shine",
      section: Section::Animations,
      badge: Badge::new()
        .label("build")
        .value("passing")
        .value_color(hex("18181b"))
        .animation(Animation::Shine)
        .logo("rust"),
    },
    Example {
      filename: "aurora-color.svg",
      alt: "Aurora (solid background)",
      section: Section::Animations,
      badge: Badge::new()
        .label("nightly")
        .value("aurora")
        .value_color(hex("1e3a8a"))
        .animation(Animation::Aurora)
        .logo("rust"),
    },
    Example {
      filename: "aurora-gradient.svg",
      alt: "Aurora (gradient background)",
      section: Section::Animations,
      badge: Badge::new()
        .value("northern lights")
        .value_gradient([hex("0f172a"), hex("164e63"), hex("312e81")])
        .animation(Animation::Aurora),
    },
    Example {
      filename: "version.svg",
      alt: "Version",
      section: Section::BuiltIns,
      badge: Badge::new().for_version("version", "1.2.0"),
    },
    Example {
      filename: "license.svg",
      alt: "License",
      section: Section::BuiltIns,
      badge: Badge::new().for_license("MIT"),
    },
    Example {
      filename: "downloads.svg",
      alt: "Downloads",
      section: Section::BuiltIns,
      badge: Badge::new().for_downloads(Period::Month, 1_234_567),
    },
    Example {
      filename: "ci-status.svg",
      alt: "CI status",
      section: Section::BuiltIns,
      badge: Badge::new().for_ci_status("build", true),
    },
    Example {
      filename: "size.svg",
      alt: "Size",
      section: Section::BuiltIns,
      badge: Badge::new().for_size("size", 1_234_567),
    },
    Example {
      filename: "rating.svg",
      alt: "Rating",
      section: Section::BuiltIns,
      badge: Badge::new().for_rating("rating", 4.5, 5.0),
    },
  ];

  for example in &examples {
    let path = output_dir.join(example.filename);
    fs::write(&path, example.badge.to_svg())?;
    println!("generated {}", path.display());
  }

  update_readme(root, &examples)?;
  println!("updated {}", root.join("readme.md").display());

  let json = Badge::new().label("version").value("v1.2.0").to_json();
  println!("to_json() -> {json}");

  Ok(())
}