use crate::diagnostic::{Diagnostic, Severity};
use crate::pipeline::PrettyMode;
use crate::value::table::{escape, table_html};
use crate::value::{Table, Value};
pub fn css() -> &'static str {
crate::TABLE_CSS
}
pub fn html_of(
values: &[Value],
closing_grid: Option<&str>,
drawing: Option<&str>,
mode: PrettyMode,
) -> Option<String> {
if mode == PrettyMode::Off {
return None;
}
if let Some(d) = drawing {
return Some(d.to_string());
}
if let Some(g) = closing_grid {
return Some(g.to_string());
}
let tables: Vec<&Table> = values
.iter()
.flat_map(|v| match v {
Value::List(items) => items.iter().collect::<Vec<_>>(),
other => vec![other],
})
.filter_map(|v| match v {
Value::Table(t) => Some(t.as_ref()),
_ => None,
})
.collect();
if !tables.is_empty() {
return Some(
tables
.iter()
.map(|t| table_grid(t))
.collect::<Vec<_>>()
.join("\n"),
);
}
if mode != PrettyMode::Always {
return None;
}
match values {
[] => None,
[Value::Record(fields)] => {
let rows: Vec<Value> = fields
.iter()
.map(|(k, v)| {
Value::record(vec![
("field".to_string(), Value::Str(k.clone())),
("value".to_string(), v.clone()),
])
})
.collect();
Some(table_html(
&rows,
Some(&["field".to_string(), "value".to_string()]),
&cell_text,
))
}
[Value::List(items)] if items.iter().all(|i| matches!(i, Value::Record(_))) => {
Some(table_html(items, None, &cell_text))
}
[Value::List(inner)] if inner.iter().all(is_scalar) => {
let scalars: Vec<&Value> = inner.iter().collect();
(scalars.len() >= 2).then(|| row_grid(&scalars))
}
items if items.iter().all(is_scalar) => {
let scalars: Vec<&Value> = items.iter().collect();
(scalars.len() >= 2).then(|| row_grid(&scalars))
}
_ => None,
}
}
fn is_scalar(v: &Value) -> bool {
!matches!(v, Value::List(_) | Value::Record(_) | Value::Table(_))
}
fn cell_text(v: &Value) -> String {
match v {
Value::Str(s) => s.clone(),
Value::Float(f) => crate::value::pretty_float(*f, 6),
other => other.display_form(),
}
}
fn cell_class(v: &Value) -> &'static str {
match v {
Value::Instant { .. } => "time",
Value::Quantity { base, .. } if base == "s" => "time",
Value::Int(_) | Value::Float(_) | Value::Quantity { .. } => "num",
Value::Bool(_) => "bool",
Value::List(_) | Value::Record(_) | Value::Table(_) => "nested",
_ => "",
}
}
pub fn table_grid(t: &Table) -> String {
let width = t.width();
let types = t.types();
let mut out = String::with_capacity(128 + t.n_rows() * width * 24);
out.push_str("<table class=\"quarb-table\">\n<thead>\n<tr>");
for j in 0..width {
let numeric = matches!(
types.get(j),
Some(
crate::value::ColumnType::Integer
| crate::value::ColumnType::Float
| crate::value::ColumnType::Quantity
| crate::value::ColumnType::Unit(_)
)
);
let mut classes: Vec<&str> = Vec::new();
if j < t.index {
classes.push("index");
}
if numeric {
classes.push("num");
}
if classes.is_empty() {
out.push_str("<th>");
} else {
out.push_str(&format!("<th class=\"{}\">", classes.join(" ")));
}
out.push_str(&format!(
"<span class=\"title\">{}</span>",
escape(&t.column_name(j))
));
let word = match t.declaration(j) {
Some(d) => d.word(),
None => types.get(j).map(|ty| ty.word()).unwrap_or_default(),
};
if !word.is_empty() {
out.push_str(&format!(" <span class=\"type\">{}</span>", escape(&word)));
}
out.push_str("</th>");
}
out.push_str("</tr>\n</thead>\n<tbody>\n");
for i in 0..t.n_rows() {
out.push_str("<tr>");
for j in 0..width {
let v = t.cell(i, j);
let mut classes: Vec<&str> = Vec::new();
if j < t.index {
classes.push("index");
}
match &v {
Value::Null => {
classes.push("null");
out.push_str(&format!("<td class=\"{}\"></td>", classes.join(" ")));
}
other => {
let c = cell_class(other);
if !c.is_empty() {
classes.push(c);
}
if classes.is_empty() {
out.push_str("<td>");
} else {
out.push_str(&format!("<td class=\"{}\">", classes.join(" ")));
}
out.push_str(&escape(&cell_text(other)));
out.push_str("</td>");
}
}
}
out.push_str("</tr>\n");
}
out.push_str("</tbody>\n</table>");
out
}
fn row_grid(values: &[&Value]) -> String {
let mut out = String::from("<table class=\"quarb-table\">\n<tbody>\n<tr>");
for v in values {
match v {
Value::Null => out.push_str("<td class=\"null\"></td>"),
other => {
let c = cell_class(other);
if c.is_empty() {
out.push_str("<td>");
} else {
out.push_str(&format!("<td class=\"{c}\">"));
}
out.push_str(&escape(&cell_text(other)));
out.push_str("</td>");
}
}
}
out.push_str("</tr>\n</tbody>\n</table>");
out
}
pub fn diagnostics_html(diagnostics: &[Diagnostic], level: Severity) -> String {
let shown: Vec<&Diagnostic> = diagnostics
.iter()
.filter(|d| d.severity != Severity::Error && d.severity <= level)
.collect();
if shown.is_empty() {
return String::new();
}
let line = |d: &Diagnostic| {
format!(
"<div class=\"quarb-diag {}\"><span class=\"diag-code\">{}</span> <span class=\"diag-site\">({})</span> {}</div>",
d.severity.word(),
escape(&d.code),
escape(&d.site),
escape(&d.message)
)
};
let mut out = String::new();
for d in shown.iter().filter(|d| d.severity == Severity::Warning) {
out.push_str(&line(d));
}
let infos: Vec<&&Diagnostic> = shown
.iter()
.filter(|d| matches!(d.severity, Severity::Info | Severity::Debug))
.collect();
if !infos.is_empty() {
out.push_str(&format!(
"<details class=\"quarb-diag-infos\"><summary>ⓘ {}</summary>",
infos.len()
));
for d in infos {
out.push_str(&line(d));
}
out.push_str("</details>");
}
out
}
pub fn document(html: &str, diagnostics: &[Diagnostic], level: Severity) -> String {
format!(
"<style>{}</style>\n{}{}",
css(),
html,
diagnostics_html(diagnostics, level)
)
}
#[cfg(test)]
mod tests {
use super::*;
fn table() -> Table {
Table::typed(
Some(vec!["Port".into(), "Draught".into()]),
vec![
vec![Value::Str("Piraeus".into()), Value::Float(18.5)],
vec![Value::Str("Koper".into()), Value::Null],
],
vec![None, Some(crate::value::ColumnType::Unit("m".into()))],
)
.unwrap()
.indexed_by("Port", None)
.unwrap()
}
#[test]
fn a_table_value_renders_as_its_grid_under_structures() {
let t = Value::table(table());
let html = html_of(std::slice::from_ref(&t), None, None, PrettyMode::Structures).unwrap();
assert!(html.contains("<th class=\"index\"><span class=\"title\">Port</span> <span class=\"type\">text</span></th>"), "{html}");
assert!(
html.contains("<span class=\"title\">Draught</span> <span class=\"type\">_m</span>"),
"{html}"
);
assert!(
html.contains("<td class=\"index\">Piraeus</td><td class=\"num\">18.5_m</td>"),
"{html}"
);
assert!(html.contains("<td class=\"null\"></td>"), "{html}");
let two = html_of(&[t.clone(), t.clone()], None, None, PrettyMode::Structures).unwrap();
assert_eq!(two.matches("<table class=\"quarb-table\">").count(), 2);
assert_eq!(html_of(&[t], None, None, PrettyMode::Off), None);
}
#[test]
fn always_renders_a_record_and_a_scalar_list_too() {
let rec = Value::record(vec![
("a".into(), Value::Int(1)),
("b".into(), Value::Str("x".into())),
]);
assert_eq!(
html_of(
std::slice::from_ref(&rec),
None,
None,
PrettyMode::Structures
),
None
);
let html = html_of(std::slice::from_ref(&rec), None, None, PrettyMode::Always).unwrap();
assert!(html.contains("<th>field</th><th>value</th>"), "{html}");
let list = Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3)]);
let html = html_of(std::slice::from_ref(&list), None, None, PrettyMode::Always).unwrap();
assert_eq!(html.matches("<tr>").count(), 1, "{html}");
assert_eq!(html.matches("<td class=\"num\">").count(), 3, "{html}");
assert_eq!(
html_of(&[Value::Int(1)], None, None, PrettyMode::Always),
None
);
}
#[test]
fn a_drawing_and_a_closing_grid_come_first() {
assert_eq!(
html_of(&[], None, Some("<svg/>"), PrettyMode::Structures).as_deref(),
Some("<svg/>")
);
assert_eq!(
html_of(&[], Some("<table/>"), None, PrettyMode::Structures).as_deref(),
Some("<table/>")
);
assert_eq!(html_of(&[], Some("<table/>"), None, PrettyMode::Off), None);
}
#[test]
fn the_diagnostics_block_follows_the_level() {
let ds = vec![
Diagnostic::new(
Severity::Warning,
"TABLE_TEXT",
"t",
"column 'x' falls to text",
),
Diagnostic::new(Severity::Info, "AUTOUNITS", "t", "autounits on t: …"),
];
let html = diagnostics_html(&ds, Severity::Warning);
assert!(
html.contains("quarb-diag warning") && !html.contains("<details"),
"{html}"
);
let html = diagnostics_html(&ds, Severity::Info);
assert!(
html.contains("<details class=\"quarb-diag-infos\"><summary>ⓘ 1</summary>"),
"{html}"
);
assert!(document("<p/>", &ds, Severity::Warning).starts_with("<style>"));
}
}