use crate::form::{Filling, Markup, Value, field_html};
use crate::list::{Cell, cells_html};
use crate::{Emit, facet::facet_html, figure::figures_html};
use crate::{meter::meter_html, placeholder::placeholder_html};
use makeover_layout::{
Accepted, CellPart, Choice, Column, Facet, FacetValue, Field, FieldKind, Figure, Meter,
Priority, Readiness, Selecting, Sort, Standing, Tone, Width,
};
use std::collections::BTreeSet;
pub(crate) fn emitted() -> BTreeSet<String> {
emitted_with(&Emit::default())
}
fn emitted_with(opts: &Emit) -> BTreeSet<String> {
let mut found = BTreeSet::new();
for html in documents(opts) {
let mut rest = html.as_str();
while let Some(at) = rest.find("class=\"") {
rest = &rest[at + "class=\"".len()..];
let end = rest.find('"').expect("an attribute closes");
for name in rest[..end].split_whitespace() {
found.insert(name.to_owned());
}
rest = &rest[end..];
}
}
found
}
fn documents(opts: &Emit) -> Vec<String> {
let mut out = vec![placeholders(opts), fields(opts), rows(opts)];
out.push(figures_html(
&[
Figure::new("42", "Tasks"),
Figure::new("12.5%", "Growth")
.change("+3")
.tone(Tone::Success),
],
opts,
));
for mode in [
Selecting::OneOf,
Selecting::AnyOf,
Selecting::Range,
Selecting::Text,
Selecting::Subtree,
] {
let values = [
FacetValue::new("music", "Music")
.standing(Standing::Taken)
.counted(128)
.at(0, true),
FacetValue::new("music/synths", "Synths")
.standing(Standing::Inherited)
.at(1, false),
FacetValue::new("music/drums", "Drums")
.standing(Standing::Pruned)
.at(1, false),
FacetValue::new("talk", "Talk").at(0, false),
];
out.push(facet_html(&Facet::new("Tag", mode, &values), opts));
}
for meter in [
Meter::new(3, 7),
Meter::new(3, 7).tone(Tone::Success).label("subtasks"),
Meter::new(9, 7).tone(Tone::Danger),
] {
out.push(meter_html(&meter, opts));
}
out
}
fn placeholders(opts: &Emit) -> String {
let mut html = String::new();
for state in [
Readiness::Ready,
Readiness::Pending,
Readiness::Empty,
Readiness::Failed,
] {
html.push_str(&placeholder_html(state, "Nothing here", None, opts));
html.push_str(&placeholder_html(
state,
"Nothing here",
Some(Markup("<button>Add one</button>")),
opts,
));
}
html
}
fn fields(opts: &Emit) -> String {
const OPTIONS: &[Choice<'_>] = &[
Choice::new("a", "The first"),
Choice::new("b", "The second").unless("Not while the first is running"),
];
const ACCEPT: &[Accepted<'_>] = &[Accepted::Type("image/png"), Accepted::Suffix(".zip")];
let mut html = String::new();
for kind in [
FieldKind::Text,
FieldKind::Secret,
FieldKind::Number,
FieldKind::Range,
FieldKind::Interval,
FieldKind::Email,
FieldKind::Url,
FieldKind::Tel,
FieldKind::Date,
FieldKind::DateTime,
FieldKind::Textarea,
FieldKind::Rich,
FieldKind::Select,
FieldKind::Radio,
FieldKind::Checkbox,
FieldKind::File,
FieldKind::Hidden,
] {
let plain = Field {
options: OPTIONS,
accept: ACCEPT,
upper_name: Some("upper"),
min: Some("0"),
max: Some("10"),
..Field::new(kind, "name", "Label")
};
let dressed = Field {
hint: Some("What it is for"),
error: Some("That will not do"),
unit: Some("minutes"),
required: true,
..plain
};
for field in [plain, dressed] {
for value in [
Value::Absent,
Value::Text("a"),
Value::On(true),
Value::Between {
lower: "1",
upper: "9",
},
] {
html.push_str(&field_html(&field, &Filling::of(value), opts));
}
}
}
html
}
fn rows(opts: &Emit) -> String {
let mut columns = Vec::new();
for (index, width) in [Width::Content, Width::Fixed, Width::Fill]
.into_iter()
.enumerate()
{
for (rank, priority) in [Priority::Optional, Priority::Secondary, Priority::Essential]
.into_iter()
.enumerate()
{
columns.push(Column {
width,
priority,
sortable: true,
sorted: Some(if rank % 2 == 0 {
Sort::Ascending
} else {
Sort::Descending
}),
..Column::new(NAMES[index * 3 + rank])
});
}
}
let parts = [
None,
Some(CellPart::Value),
Some(CellPart::Tokens),
Some(CellPart::Actions),
Some(CellPart::Link),
];
let mut html = String::new();
for part in parts {
let cells: Vec<Cell<'_>> = columns
.iter()
.map(|column| Cell {
column: column.name,
part,
content: Markup("<span>x</span>"),
})
.collect();
html.push_str(&cells_html(&columns, &cells, opts));
}
html
}
const NAMES: [&str; 9] = ["a", "b", "c", "d", "e", "f", "g", "h", "i"];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_class_this_crate_emits_is_one_its_vocabulary_names() {
let opts = Emit::default();
let names = crate::vocabulary::names(&opts);
let emitted = emitted();
assert!(
emitted.len() > 30,
"the corpus rendered {} classes, which reads as the corpus having \
stopped calling the emitters rather than the crate having shrunk",
emitted.len()
);
let stranger: Vec<&String> = emitted
.iter()
.filter(|class| !class.starts_with("col-"))
.filter(|class| !names.contains(*class))
.collect();
assert!(
stranger.is_empty(),
"{} class(es) emitted that `vocabulary::names` does not hold. Give \
them a rule, or add them to the unruled list `names` reads:\n{}",
stranger.len(),
stranger
.iter()
.map(|c| format!(" .{c}"))
.collect::<Vec<_>>()
.join("\n")
);
}
#[test]
fn a_class_prefix_reaches_every_class_but_the_states() {
let opts = Emit {
class_prefix: "mk-",
..Emit::default()
};
let names = crate::vocabulary::names(&opts);
let stranger: Vec<String> = emitted_with(&opts)
.into_iter()
.filter(|class| !class.starts_with("mk-col-"))
.filter(|class| !names.contains(class))
.collect();
assert!(
stranger.is_empty(),
"{} class(es) a prefixed render emits that `vocabulary::names` does \
not hold:\n{}",
stranger.len(),
stranger
.iter()
.map(|c| format!(" .{c}"))
.collect::<Vec<_>>()
.join("\n")
);
}
}