use crate::form::escape_into;
use crate::{Emit, push_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 = String::new();
figure_html_into(figure, opts, &mut html);
html
}
pub fn figure_html_into(figure: &Figure<'_>, opts: &Emit, out: &mut String) {
out.push_str("<div class=\"");
push_class(out, "figure", opts);
out.push_str("\" aria-label=\"");
escape_into(figure.caption, out);
out.push_str(": ");
escape_into(figure.value, out);
if let Some(change) = figure.change {
out.push_str(", ");
escape_into(change, out);
}
out.push('"');
if figure.tone != Tone::Neutral {
let _ = write!(out, " data-tone=\"{}\"", figure.tone.token());
}
out.push_str("><span class=\"");
push_class(out, "figure-value", opts);
out.push_str("\" aria-hidden=\"true\">");
escape_into(figure.value, out);
out.push_str("</span><span class=\"");
push_class(out, "figure-caption", opts);
out.push_str("\" aria-hidden=\"true\">");
escape_into(figure.caption, out);
out.push_str("</span>");
if let Some(change) = figure.change {
out.push_str("<span class=\"");
push_class(out, "figure-change", opts);
out.push_str("\" aria-hidden=\"true\">");
escape_into(change, out);
out.push_str("</span>");
}
out.push_str("</div>");
}
#[must_use]
pub fn figures_html(figures: &[Figure<'_>], opts: &Emit) -> String {
let mut html = String::new();
figures_html_into(figures, opts, &mut html);
html
}
pub fn figures_html_into(figures: &[Figure<'_>], opts: &Emit, out: &mut String) {
out.push_str("<div class=\"");
push_class(out, "figures", opts);
out.push_str("\">");
for figure in figures {
figure_html_into(figure, opts, out);
}
out.push_str("</div>");
}
#[cfg(test)]
mod tests {
use super::*;
use crate::form::escape;
#[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 a_streamed_figure_is_the_figure_the_other_form_returns() {
let opts = Emit {
class_prefix: "mo-",
..Emit::default()
};
for figure in [
Figure::new("17", "Total"),
Figure::new("<b>3</b>", "a & b").tone(Tone::Danger),
Figure::new("1,204", "Views & co").change("+12.5% <up>"),
] {
let mut streamed = String::new();
figure_html_into(&figure, &opts, &mut streamed);
assert_eq!(streamed, figure_html(&figure, &opts));
assert!(
streamed.contains(&format!("aria-label=\"{}\"", escape(&figure_text(&figure)))),
"{streamed}"
);
let mut strip = String::new();
figures_html_into(&[figure], &opts, &mut strip);
assert_eq!(strip, figures_html(&[figure], &opts));
}
}
#[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""#));
}
}