use crate::i18n::{self, Lang};
pub const ASSET_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FlashKind {
Error,
Info,
Success,
}
impl FlashKind {
const fn parts(self) -> (&'static str, &'static str, &'static str, &'static str) {
match self {
Self::Error => ("error", "x-circle", r#" role="alert""#, "ui.flash_error"),
Self::Info => ("info", "info-circle", "", "ui.flash_info"),
Self::Success => (
"success",
"check-circle",
r#" role="status""#,
"ui.flash_success",
),
}
}
}
#[must_use]
pub fn flash_open(kind: FlashKind, lang: Lang) -> String {
let (class, symbol, role, word_key) = kind.parts();
format!(
r#"<div class="flash {class}"{role}>{icon}<span class="sr-only">{word}</span> "#,
icon = icon(symbol),
word = i18n::t(lang, word_key),
)
}
#[must_use]
pub fn flash(kind: FlashKind, lang: Lang, body_html: &str) -> String {
format!("{}{body_html}</div>", flash_open(kind, lang))
}
#[must_use]
pub fn icon(name: &'static str) -> String {
format!(
r##"<svg class="icon" aria-hidden="true"><use href="/static/img/icons.svg?v={ASSET_VERSION}#{name}"></use></svg>"##
)
}
#[cfg(test)]
mod tests {
use super::*;
const SPRITE: &str = include_str!("../../static/img/icons.svg");
#[test]
fn an_icon_is_decorative_and_points_at_the_sprite_symbol() {
let svg = icon("house");
assert!(svg.contains(r#"aria-hidden="true""#), "{svg}");
assert!(!svg.contains("focusable"), "{svg}");
assert!(svg.contains("/static/img/icons.svg?v="), "{svg}");
assert!(svg.ends_with("#house\"></use></svg>"), "{svg}");
}
#[test]
fn an_error_flash_is_an_alert_with_an_icon_and_a_spoken_word() {
let html = flash_open(FlashKind::Error, Lang::En);
assert!(
html.starts_with(r#"<div class="flash error" role="alert">"#),
"{html}"
);
assert!(html.contains("#x-circle"), "{html}");
assert!(
html.contains(r#"<span class="sr-only">Error:</span> "#),
"{html}"
);
}
#[test]
fn a_success_flash_is_a_polite_status() {
let html = flash_open(FlashKind::Success, Lang::En);
assert!(
html.starts_with(r#"<div class="flash success" role="status">"#),
"{html}"
);
assert!(html.contains("#check-circle"), "{html}");
assert!(
html.contains(r#"<span class="sr-only">Success:</span> "#),
"{html}"
);
}
#[test]
fn an_info_flash_announces_nothing() {
let html = flash_open(FlashKind::Info, Lang::En);
assert!(html.starts_with(r#"<div class="flash info">"#), "{html}");
assert!(!html.contains("role="), "{html}");
assert!(html.contains("#info-circle"), "{html}");
}
#[test]
fn flash_wraps_the_body_and_closes_the_box() {
let html = flash(FlashKind::Error, Lang::En, "<bad>");
assert!(html.ends_with("<bad></div>"), "{html}");
}
#[test]
fn the_sprite_is_inert_markup_only() {
let lower = SPRITE.to_ascii_lowercase();
for forbidden in [
"<script",
"<style",
"onload",
"onclick",
"javascript:",
"href=\"http",
"<foreignobject",
] {
assert!(
!lower.contains(forbidden),
"{forbidden} found in the sprite"
);
}
}
#[test]
fn every_icon_the_pages_use_exists_in_the_sprite() {
let sources = [
include_str!("layout.rs"),
include_str!("home.rs"),
include_str!("csaf.rs"),
include_str!("admin.rs"),
include_str!("settings.rs"),
include_str!("info.rs"),
];
let mut used = std::collections::BTreeSet::new();
for source in sources {
for pattern in ["icon(\"", "icon: \""] {
for part in source.split(pattern).skip(1) {
if let Some(name) = part.split('"').next() {
used.insert(name.to_owned());
}
}
}
}
assert!(
used.len() >= 8,
"expected the pages to use several icons: {used:?}"
);
for name in &used {
assert!(
SPRITE.contains(&format!(r#"<symbol id="{name}" viewBox"#)),
"icon {name} is used but missing from icons.svg"
);
}
}
}