use base64::Engine;
use base64::prelude::BASE64_STANDARD;
use serde::Deserialize;
use serde::de::IntoDeserializer;
use super::_width::WIDTHS;
use super::Color;
#[cfg(feature = "simple-icons")]
use crate::_icons::ICONS;
pub(crate) fn empty_string_as_none<'de, D, T>(de: D) -> Result<Option<T>, D::Error>
where
D: serde::Deserializer<'de>,
T: serde::Deserialize<'de>,
{
let opt: Option<String> = Option::deserialize(de)?;
match opt.as_deref() {
None | Some("") => Ok(None),
Some(s) => T::deserialize(s.into_deserializer()).map(Some),
}
}
pub fn cacl_width(text: &str) -> f32 {
let fallback_width = WIDTHS[64]; let mut total_width = 0.0;
for ch in text.chars() {
let index = ch as usize;
let width = WIDTHS.get(index).copied().unwrap_or(fallback_width);
total_width += width;
}
total_width
}
pub fn millify(n: u64) -> String {
let mut n = n as f64;
let mut i = 0;
let units = ["", "k", "M", "B", "T"];
while n >= 1_000.0 && i < units.len() - 1 {
n /= 1_000.0;
i += 1;
}
let label = if n >= 100.0 { format!("{n:.0}") } else { format!("{n:.1}") };
let label = label.strip_suffix(".0").unwrap_or(&label);
let label = format!("{label}{}", units[i]);
label
}
pub fn millify_iec(n: u64) -> String {
let mut n = n as f64;
let mut i = 0;
let units = ["", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"];
while n >= 1_024.0 && i < units.len() - 1 {
n /= 1_024.0;
i += 1;
}
let label = format!("{n:.1}");
let label = label.strip_suffix(".0").unwrap_or(&label);
let label = format!("{label} {}", units[i]);
label
}
pub(crate) fn to_icon_uri(svg: &str) -> String {
format!("data:image/svg+xml;base64,{}", BASE64_STANDARD.encode(svg))
}
#[cfg(feature = "simple-icons")]
fn get_icon_path(name: &str) -> Option<&'static str> {
let name = name.to_lowercase();
let candidates = [
name.clone(),
name.replace('-', "").replace("!", "").replace("_", "").replace(" ", ""),
name.replace('.', "dot").replace("+", "plus"),
];
candidates.iter().find_map(|candidate| ICONS.get(candidate)).copied()
}
#[cfg(feature = "simple-icons")]
pub(crate) fn has_icon(name: &str) -> bool {
get_icon_path(name).is_some()
}
#[cfg(feature = "simple-icons")]
pub(crate) fn get_icon(name: &str, color: &Color) -> Option<String> {
let path = get_icon_path(name)?;
let icon = format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" role="img" viewBox="0 0 24 24" fill="{}"><path d="{}" /></svg>"#,
color.to_css(),
path
);
Some(to_icon_uri(&icon))
}
pub(crate) fn text_color(bg_color: &Color) -> Color {
let hex = bg_color.to_hex();
let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0) as f32 / 255.0;
let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0) as f32 / 255.0;
let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0) as f32 / 255.0;
let luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
if luminance > 0.85 { Color::Black } else { Color::White }
}
#[derive(Clone, Copy, Ord, PartialOrd, Eq, PartialEq)]
enum LicenseKind {
Unknown,
Copyleft,
Permissive,
PublicDomain,
}
fn license_kind(value: &str) -> LicenseKind {
match value.trim() {
"AFL-3.0" | "APACHE-2.0" | "APACHE 2.0" | "APACHE LICENSE 2.0" | "ARTISTIC-2.0" | "BSD"
| "BSD-2-CLAUSE" | "BSD-3-CLAUSE" | "BSD-3-CLAUSE-CLEAR" | "BSL-1.0" | "CC-BY-4.0"
| "ECL-2.0" | "ISC" | "MIT" | "MIT LICENSE" | "MS-PL" | "NCSA" | "POSTGRESQL" | "ZLIB" => {
LicenseKind::Permissive
}
"AGPL-1.0-ONLY" | "AGPL-1.0-OR-LATER" | "AGPL-3.0" | "AGPL-3.0-ONLY" | "AGPL-3.0-OR-LATER"
| "AGPLV3+" | "CC-BY-SA-4.0" | "EPL" | "EPL-1.0" | "EPL-2.0" | "EUPL-1.1" | "GPL"
| "GPL-1.0-ONLY" | "GPL-1.0-OR-LATER" | "GPL-2.0" | "GPL-2.0-ONLY" | "GPL-2.0-OR-LATER"
| "GPL-3.0" | "GPL-3.0-ONLY" | "GPL-3.0-OR-LATER" | "GPLV2" | "GPLV2+" | "GPLV3" | "GPLV3+"
| "LGPL" | "LGPL-2.0-ONLY" | "LGPL-2.0-OR-LATER" | "LGPL-2.1" | "LGPL-2.1-ONLY"
| "LGPL-2.1-OR-LATER" | "LGPL-3.0" | "LGPL-3.0-ONLY" | "LGPL-3.0-OR-LATER" | "LGPLV2"
| "LGPLV2+" | "LGPLV3" | "LGPLV3+" | "LPPL-1.3C" | "MPL" | "MPL 1.1" | "MPL 2.0"
| "MPL-2.0" | "MS-RL" | "OFL-1.1" | "OSL-3.0" => LicenseKind::Copyleft,
"0BSD" | "CC0" | "CC0-1.0" | "UNLICENSE" | "WTFPL" => LicenseKind::PublicDomain,
_ => LicenseKind::Unknown,
}
}
pub(crate) fn license_color(license: &str) -> Color {
let normalized = license.split_whitespace().collect::<Vec<_>>().join(" ").to_uppercase();
let normalized = normalized.replace(" OR ", "|").replace(" AND ", "|").replace(" WITH ", "|");
let kind = normalized
.split(['|', ',', '/', ';', '&', '(', ')'])
.flat_map(|part| std::iter::once(part).chain(part.split_ascii_whitespace()))
.map(license_kind)
.max()
.unwrap_or(LicenseKind::Unknown);
match kind {
LicenseKind::Permissive => Color::Blue,
LicenseKind::Copyleft => Color::Orange,
LicenseKind::PublicDomain => Color::Lime,
LicenseKind::Unknown => Color::Gray,
}
}
pub(crate) fn rating_color<T: Into<f64>>(value: T, max_value: T) -> Color {
let (value, max_value) = (value.into(), max_value.into());
if !value.is_finite() || !max_value.is_finite() || max_value <= 0.0 {
return Color::Red;
}
let score = (value / max_value).clamp(0.0, 1.0);
match score {
x if x >= 0.80 => Color::Green,
x if x >= 0.60 => Color::Lime,
x if x >= 0.40 => Color::Yellow,
x if x >= 0.20 => Color::Orange,
_ => Color::Red,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_millify_uses_adaptive_precision_for_every_scale() {
for (value, expected) in [
(1_200, "1.2k"),
(12_300, "12.3k"),
(110_400, "110k"),
(1_200_000, "1.2M"),
(12_300_000, "12.3M"),
(110_400_000, "110M"),
(1_200_000_000, "1.2B"),
(12_300_000_000, "12.3B"),
(110_400_000_000, "110B"),
(1_200_000_000_000, "1.2T"),
(12_300_000_000_000, "12.3T"),
(110_400_000_000_000, "110T"),
] {
assert_eq!(millify(value), expected);
}
}
#[test]
fn test_millify_scale_boundaries() {
for (value, expected) in [
(0, "0"),
(999, "999"),
(1_000, "1k"),
(999_000, "999k"),
(1_000_000, "1M"),
(999_000_000, "999M"),
(1_000_000_000, "1B"),
(999_000_000_000, "999B"),
(1_000_000_000_000, "1T"),
(999_000_000_000_000, "999T"),
] {
assert_eq!(millify(value), expected);
}
}
#[test]
fn test_millify_iec_supports_the_full_u64_range() {
for (value, expected) in
[(1_u64 << 40, "1 TiB"), (1_u64 << 50, "1 PiB"), (1_u64 << 60, "1 EiB"), (u64::MAX, "16 EiB")]
{
assert_eq!(millify_iec(value), expected);
}
}
}