use std::sync::LazyLock;
use axum::response::Response;
use crate::web::assets::Assets;
pub const ICONDATA_LU_VERSION: &str = "0.1.0";
const ICONS: &[(&str, &icondata_core::IconData)] = &[
("dashboard", icondata_lu::LuLayoutDashboard),
("log", icondata_lu::LuScrollText),
("blacklist", icondata_lu::LuShieldBan),
("allowlist", icondata_lu::LuShieldCheck),
("local", icondata_lu::LuServer),
("blocklists", icondata_lu::LuListChecks),
("upstreams", icondata_lu::LuGlobe),
("forwarding", icondata_lu::LuRouter),
("settings", icondata_lu::LuSettings),
("menu", icondata_lu::LuMenu),
("close", icondata_lu::LuX),
("pause", icondata_lu::LuPause),
("logout", icondata_lu::LuLogOut),
("sun", icondata_lu::LuSun),
("moon", icondata_lu::LuMoon),
("add", icondata_lu::LuPlus),
("remove", icondata_lu::LuTrash2),
("refresh", icondata_lu::LuRefreshCw),
("save", icondata_lu::LuSave),
("toggle", icondata_lu::LuPower),
("older", icondata_lu::LuChevronDown),
("live", icondata_lu::LuActivity),
("system", icondata_lu::LuServer),
("history", icondata_lu::LuHistory),
("health", icondata_lu::LuHeartPulse),
("talkers", icondata_lu::LuTrendingUp),
("queries", icondata_lu::LuActivity),
("blocked", icondata_lu::LuBan),
("ratio", icondata_lu::LuPercent),
("cached", icondata_lu::LuDatabase),
("forwarded", icondata_lu::LuForward),
("domains", icondata_lu::LuList),
("clients", icondata_lu::LuUsers),
("version", icondata_lu::LuTag),
("uptime", icondata_lu::LuClock),
("qps", icondata_lu::LuGauge),
("memory", icondata_lu::LuMemoryStick),
];
static SPRITE: LazyLock<String> = LazyLock::new(Icons::build_sprite);
pub struct Icons;
impl Icons {
pub async fn sprite() -> Response {
Assets::serve(SPRITE.as_bytes(), "image/svg+xml; charset=utf-8")
}
pub(crate) fn sprite_bytes() -> &'static [u8] {
SPRITE.as_bytes()
}
fn build_sprite() -> String {
let mut out =
String::from(r#"<svg xmlns="http://www.w3.org/2000/svg" style="display:none">"#);
for (id, icon) in ICONS {
Self::push_symbol(&mut out, id, icon);
}
out.push_str("</svg>");
out
}
fn push_symbol(out: &mut String, id: &str, icon: &icondata_core::IconData) {
use std::fmt::Write;
let _ = write!(out, r#"<symbol id="{id}""#);
for (attr, value) in [
("viewBox", icon.view_box),
("fill", icon.fill),
("stroke", icon.stroke),
("stroke-width", icon.stroke_width),
("stroke-linecap", icon.stroke_linecap),
("stroke-linejoin", icon.stroke_linejoin),
] {
if let Some(value) = value {
let _ = write!(out, r#" {attr}="{value}""#);
}
}
let _ = write!(out, ">{}</symbol>", icon.data);
}
}
#[cfg(test)]
mod tests {
use super::*;
use axum::http::{StatusCode, header};
#[test]
fn sprite_is_well_formed() {
let sprite = Icons::build_sprite();
assert!(sprite.starts_with("<svg"), "sprite must open with <svg>");
assert!(
sprite.ends_with("</svg>"),
"sprite must close the root <svg>"
);
assert!(sprite.contains("currentColor"));
}
#[test]
fn every_registered_icon_emits_a_symbol() {
let sprite = Icons::build_sprite();
for (id, _) in ICONS {
assert!(
sprite.contains(&format!(r#"<symbol id="{id}""#)),
"sprite missing symbol for {id:?}"
);
}
}
#[tokio::test]
async fn sprite_route_sets_svg_content_type_and_cache() {
let resp = Icons::sprite().await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(),
"image/svg+xml; charset=utf-8"
);
assert!(
resp.headers()
.get(header::CACHE_CONTROL)
.is_some_and(|v| v.to_str().unwrap().contains("immutable")),
"sprite must be served with an immutable cache header"
);
}
}