use crate::form::escape;
use crate::{Emit, class};
use makeover_layout::{Figure, Intent, Tone};
use std::fmt::Write as _;
#[must_use]
pub fn figure_text(figure: &Figure<'_>) -> String {
figure.change.map_or_else(
|| format!("{}: {}", figure.caption, figure.value),
|change| format!("{}: {}, {change}", figure.caption, figure.value),
)
}
#[must_use]
pub fn figure_html(figure: &Figure<'_>, opts: &Emit) -> String {
let mut html = format!(
"<div class=\"{}\" aria-label=\"{}\"",
class("figure", opts),
escape(&figure_text(figure))
);
if figure.tone != Tone::Neutral {
let _ = write!(html, " data-tone=\"{}\"", figure.tone.token());
}
let _ = write!(
html,
"><span class=\"{}\" aria-hidden=\"true\">{}</span>\
<span class=\"{}\" aria-hidden=\"true\">{}</span>",
class("figure-value", opts),
escape(figure.value),
class("figure-caption", opts),
escape(figure.caption),
);
if let Some(change) = figure.change {
let _ = write!(
html,
"<span class=\"{}\" aria-hidden=\"true\">{}</span>",
class("figure-change", opts),
escape(change),
);
}
html.push_str("</div>");
html
}
#[must_use]
pub fn figures_html(figures: &[Figure<'_>], opts: &Emit) -> String {
let mut html = format!("<div class=\"{}\">", class("figures", opts));
for figure in figures {
html.push_str(&figure_html(figure, opts));
}
html.push_str("</div>");
html
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_noun_reaches_a_reader_before_the_number() {
let figure = Figure::new("17", "Current Streak");
assert_eq!(figure_text(&figure), "Current Streak: 17");
let html = figure_html(&figure, &Emit::default());
assert!(html.contains(r#"aria-label="Current Streak: 17""#));
assert_eq!(html.matches(r#"aria-hidden="true""#).count(), 2);
}
#[test]
fn a_change_is_its_own_span_and_reaches_a_reader_through_the_name() {
let figure = Figure::new("1,204", "Views").change("+12.5%");
assert_eq!(figure_text(&figure), "Views: 1,204, +12.5%");
let html = figure_html(&figure, &Emit::default());
assert!(html.contains("figure-change"), "{html}");
assert!(html.contains(">+12.5%<"), "{html}");
assert_eq!(html.matches(r#"aria-hidden="true""#).count(), 3);
let plain = figure_html(&Figure::new("17", "Total"), &Emit::default());
assert!(!plain.contains("figure-change"), "{plain}");
}
#[test]
fn a_change_carries_no_tone_of_its_own() {
let html = figure_html(
&Figure::new("1,204", "Views")
.change("-4%")
.tone(Tone::Danger),
&Emit::default(),
);
assert_eq!(html.matches("data-tone").count(), 1, "{html}");
}
#[test]
fn a_change_is_text_and_cannot_become_markup() {
let html = figure_html(
&Figure::new("1", "Views").change("<img src=x onerror=alert(1)>"),
&Emit::default(),
);
assert!(!html.contains("<img"), "{html}");
}
#[test]
fn an_untoned_figure_emits_no_tone_attribute() {
let plain = figure_html(&Figure::new("17", "Total"), &Emit::default());
assert!(!plain.contains("data-tone"));
let toned = figure_html(
&Figure::new("0", "Current Streak").tone(Tone::Warning),
&Emit::default(),
);
assert!(toned.contains(r#"data-tone="warning""#));
}
#[test]
fn a_value_and_a_caption_are_escaped_like_every_other_string() {
let html = figure_html(&Figure::new("<b>3</b>", "a & b"), &Emit::default());
assert!(html.contains("a & b"));
assert!(html.contains("<b>"));
assert!(!html.contains("<b>"));
}
#[test]
fn a_strip_is_the_unit_because_a_renderer_cannot_infer_a_set() {
let html = figures_html(
&[
Figure::new("17", "Current Streak"),
Figure::new("84%", "Completion Rate"),
],
&Emit::default(),
);
assert!(html.starts_with(r#"<div class="figures">"#));
assert_eq!(html.matches(r#"class="figure""#).count(), 2);
}
#[test]
fn an_empty_strip_renders_as_an_empty_strip() {
let html = figures_html(&[], &Emit::default());
assert_eq!(html, r#"<div class="figures"></div>"#);
assert!(!html.contains("figure-"));
}
#[test]
fn the_prefix_reaches_every_class() {
let opts = Emit {
class_prefix: "mo-",
..Emit::default()
};
let html = figures_html(&[Figure::new("17", "Total")], &opts);
assert!(html.contains(r#"class="mo-figures""#));
assert!(html.contains(r#"class="mo-figure""#));
assert!(html.contains(r#"class="mo-figure-value""#));
assert!(html.contains(r#"class="mo-figure-caption""#));
}
}