use crate::form::escape_into;
use crate::{Emit, push_class};
use makeover_layout::{Intent, Meter, Tone};
use std::fmt::Write as _;
#[must_use]
pub fn meter_text(meter: &Meter<'_>) -> String {
match meter.label {
Some(label) => format!("{} of {} {label}", meter.done, meter.total),
None => format!("{} of {}", meter.done, meter.total),
}
}
#[must_use]
pub fn meter_html(meter: &Meter<'_>, opts: &Emit) -> String {
let mut html = String::new();
meter_html_into(meter, opts, &mut html);
html
}
pub fn meter_html_into(meter: &Meter<'_>, opts: &Emit, out: &mut String) {
let reported = meter.done.min(meter.total);
out.push_str("<div class=\"");
push_class(out, "progress", opts);
let _ = write!(
out,
"\" role=\"progressbar\" aria-valuenow=\"{reported}\" \
aria-valuemin=\"0\" aria-valuemax=\"{}\" aria-label=\"{} of {}",
meter.total, meter.done, meter.total
);
if let Some(label) = meter.label {
out.push(' ');
escape_into(label, out);
}
out.push_str("\">");
out.push_str("<div class=\"");
push_class(out, "progress-fill", opts);
out.push('"');
if meter.tone != Tone::Neutral {
let _ = write!(out, " data-tone=\"{}\"", meter.tone.token());
}
if meter.overflowing() {
out.push_str(" data-over=\"true\"");
}
let _ = write!(out, " style=\"width: {}%\"></div></div>", meter.percent());
}
#[cfg(test)]
mod tests {
use super::*;
use crate::form::escape;
#[test]
fn a_streamed_meter_is_the_meter_the_other_form_returns() {
let opts = Emit {
class_prefix: "mk-",
..Emit::default()
};
for meter in [
Meter::new(0, 0),
Meter::new(3, 7).label("sub & tasks"),
Meter::new(9, 7).tone(Tone::Danger).label("<tasks>"),
] {
let mut streamed = String::new();
meter_html_into(&meter, &opts, &mut streamed);
assert_eq!(streamed, meter_html(&meter, &opts));
assert!(
streamed.contains(&format!("aria-label=\"{}\"", escape(&meter_text(&meter)))),
"{streamed}"
);
}
}
#[test]
fn a_full_bar_says_whether_it_ran_over() {
let exact = meter_html(&Meter::new(30, 30), &Emit::default());
let over = meter_html(&Meter::new(45, 30), &Emit::default());
assert!(exact.contains("width: 100%"));
assert!(over.contains("width: 100%"));
assert!(!exact.contains("data-over"));
assert!(over.contains(r#"data-over="true""#));
}
#[test]
fn the_accessible_name_keeps_the_number_the_bar_cannot_show() {
let over = Meter::new(45, 30).label("minutes");
assert_eq!(meter_text(&over), "45 of 30 minutes");
assert!(meter_html(&over, &Emit::default()).contains(r#"aria-label="45 of 30 minutes""#));
}
#[test]
fn aria_valuenow_stays_inside_its_range() {
let html = meter_html(&Meter::new(45, 30), &Emit::default());
assert!(html.contains(r#"aria-valuenow="30""#));
assert!(html.contains(r#"aria-valuemax="30""#));
}
#[test]
fn an_untoned_bar_emits_no_tone_attribute() {
let plain = meter_html(&Meter::new(1, 2), &Emit::default());
assert!(!plain.contains("data-tone"));
let toned = meter_html(&Meter::new(1, 2).tone(Tone::Danger), &Emit::default());
assert!(toned.contains(r#"data-tone="danger""#));
}
#[test]
fn an_empty_set_renders_an_empty_trough() {
let html = meter_html(&Meter::new(0, 0), &Emit::default());
assert!(html.contains("width: 0%"));
assert!(html.contains(r#"aria-valuemax="0""#));
}
#[test]
fn the_label_is_escaped_like_every_other_string() {
let html = meter_html(&Meter::new(1, 2).label("a & b"), &Emit::default());
assert!(html.contains("a & b"));
assert!(!html.contains("a & b"));
}
#[test]
fn the_prefix_reaches_both_classes() {
let opts = Emit {
class_prefix: "mo-",
..Emit::default()
};
let html = meter_html(&Meter::new(1, 2), &opts);
assert!(html.contains(r#"class="mo-progress""#));
assert!(html.contains(r#"class="mo-progress-fill""#));
}
}