use datagrid_core::{CellAlign, ExportAggregate, ExportRow, ExportTable};
use std::fmt::Write as _;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct HtmlOptions {
pub caption: Option<String>,
pub document: bool,
pub lang: Option<String>,
pub style: bool,
}
pub const PRINT_CSS: &str = "\
@page { margin: 12mm; }\
table.datagrid { width: 100%; border-collapse: collapse; font: 10pt/1.35 system-ui, sans-serif; }\
table.datagrid caption { margin-bottom: 0.5em; font-weight: 600; text-align: start; }\
table.datagrid th, table.datagrid td { padding: 3pt 6pt; border-bottom: 0.5pt solid #999; }\
table.datagrid thead { display: table-header-group; }\
table.datagrid thead th { border-bottom: 1pt solid #000; text-align: start; }\
table.datagrid tr { break-inside: avoid; }\
table.datagrid .end { text-align: end; }\
table.datagrid .center { text-align: center; }\
table.datagrid .group th { background: #eee; text-align: start; }\
table.datagrid .aggregates td, table.datagrid .totals td { font-weight: 600; }\
table.datagrid .totals td { border-top: 1pt solid #000; }";
#[must_use]
pub fn to_html(table: &ExportTable, options: &HtmlOptions) -> String {
let mut out = String::new();
if options.document {
out.push_str("<!doctype html>\n<html");
if let Some(lang) = &options.lang {
let _ = write!(out, " lang=\"{}\"", escape(lang));
}
out.push_str(">\n<head><meta charset=\"utf-8\"><style>");
out.push_str(PRINT_CSS);
out.push_str("</style></head>\n<body>\n");
}
if options.style && !options.document {
let _ = writeln!(out, "<style>{PRINT_CSS}</style>");
}
out.push_str("<table class=\"datagrid\">\n");
if let Some(caption) = &options.caption {
let _ = writeln!(out, "<caption>{}</caption>", escape(caption));
}
header(&mut out, table);
out.push_str("<tbody>\n");
let width = table.columns.len().max(1);
for row in &table.rows {
match row {
ExportRow::Data(cells) => {
out.push_str("<tr>");
for (index, cell) in cells.iter().enumerate() {
let _ = write!(
out,
"<td{}>{}</td>",
align_attr(table, index),
escape(&cell.text)
);
}
out.push_str("</tr>\n");
}
ExportRow::GroupHeader {
level,
caption,
aggregates,
..
} => {
let _ = writeln!(
out,
"<tr class=\"group\"><th colspan=\"{width}\" scope=\"colgroup\" style=\"padding-inline-start:{}em\">{}</th></tr>",
1 + level * 2,
escape(caption)
);
aggregate_rows(&mut out, table, aggregates, "aggregates");
}
ExportRow::Aggregates { label, cells, .. } => {
let class = if label.is_some() {
"totals"
} else {
"aggregates"
};
aggregate_rows(&mut out, table, cells, class);
}
_ => {}
}
}
out.push_str("</tbody>\n</table>\n");
if options.document {
out.push_str("</body>\n</html>\n");
}
out
}
fn header(out: &mut String, table: &ExportTable) {
let levels = table
.columns
.iter()
.map(|column| column.group_path.len())
.max()
.unwrap_or(0);
out.push_str("<thead>\n");
for level in 0..levels {
out.push_str("<tr>");
let mut column = 0;
while column < table.columns.len() {
let here = table.columns.get(column);
let label = here.and_then(|column| column.group_path.get(level));
let mut span = 1;
if label.is_some() {
let prefix = here.and_then(|column| column.group_path.get(..=level));
while let Some(next) = table.columns.get(column + span) {
if next.group_path.get(..=level) != prefix {
break;
}
span += 1;
}
}
let _ = write!(
out,
"<th colspan=\"{span}\" scope=\"colgroup\" class=\"center\">{}</th>",
label.map_or_else(String::new, |label| escape(label))
);
column += span;
}
out.push_str("</tr>\n");
}
out.push_str("<tr>");
for (index, column) in table.columns.iter().enumerate() {
let _ = write!(
out,
"<th scope=\"col\"{}>{}</th>",
align_attr(table, index),
escape(&column.label)
);
}
out.push_str("</tr>\n</thead>\n");
}
fn aggregate_rows(
out: &mut String,
table: &ExportTable,
columns: &[Vec<ExportAggregate>],
class: &str,
) {
let rows = columns.iter().map(Vec::len).max().unwrap_or(0);
for index in 0..rows {
let _ = write!(out, "<tr class=\"{class}\">");
for column in 0..table.columns.len() {
let text = columns
.get(column)
.and_then(|aggregates| aggregates.get(index))
.filter(|aggregate| aggregate.value.is_some())
.map(|aggregate| format!("{} {}", aggregate.name, aggregate.text))
.unwrap_or_default();
let _ = write!(
out,
"<td{}>{}</td>",
align_attr(table, column),
escape(&text)
);
}
out.push_str("</tr>\n");
}
}
fn align_attr(table: &ExportTable, column: usize) -> &'static str {
match table.columns.get(column).map(|column| column.align) {
Some(CellAlign::End) => " class=\"end\"",
Some(CellAlign::Center) => " class=\"center\"",
_ => "",
}
}
fn escape(text: &str) -> String {
let mut out = String::with_capacity(text.len());
for c in text.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
c => out.push(c),
}
}
out
}